# Pereval — full partner documentation Pereval (pereval.dating) connects dating apps into one network. This file contains every guide and, at the end, the complete OpenAPI 3.1 specification (the source of truth for fields, limits and errors). # Platform documentation Pereval connects dating apps and websites into one network. Your user stays with you, but sees people from other apps, likes them and chats with them — with translation if their languages differ. Basic integration and exchanging profiles, ordinary likes, matches and messages are free. Additional services are optional. We are just launching the network: external profiles may be few or absent. If you join first, cross-platform introductions become possible once other platforms join. You can test integration in the sandbox and prepare for growth. We are preparing optional-service bonuses for early participants; amounts and terms will be published separately. [For founding platforms](/en/platforms/#early-platforms). ## Where to start Pick the start that matches the product. It stands alone: what to store, which requests, and when the integration is done. The full rules and every field are in the sections below and in the [API reference](/en/docs/reference/). | Your product | Start | |---|---| | an app where a person flips through profiles one by one | [Swipe app](/en/docs/guide-app/) | | a site with filters and pages of profiles | [Search site](/en/docs/guide-site/) | | a bot, and you have no webhook URL | [Bot](/en/docs/guide-bot/) | | you want to test the API and sandbox with curl | [Test the API in the sandbox](/en/docs/quickstart/) | | you have not chosen how to connect yet | [Ways to integrate](/en/docs/integration/) | | an AI agent will write the integration | [For AI agents](/en/docs/ai-agents/) — the agent needs this page and the start for the product | ## From application to launch 1. **[Apply](/en/apply/).** We review the platform by hand and open the dashboard for you. 2. **Test the API in the test environment.** Issue a test key, send a `ping` and run the sandbox scenario. The [15-minute API check](/en/docs/quickstart/) confirms that requests and events work; it is not a finished integration in your product. 3. **Build the user flow into your product.** Add opt-in consent, profile sync, the network feed, and handling for likes, matches, messages and events. Choose a [start for an app, website or bot](/en/docs/integration/). 4. **Pass the checklist and enable the live environment.** Verify consent, deletion, event signatures and user-facing notices, then issue a live key in the dashboard. See the full [testing checklist](/en/docs/testing/#pre-launch-checklist). ## What to build into your product 1. **User consent.** A "Show me in the Pereval network" toggle in profile settings, off by default (and, optionally, "Open to chatting in other languages"). Without consent turned on, the profile is not sent to the network. 2. **Sending profiles.** When a person turns on participation or changes their profile — `PUT /v1/members/{your_user_id}`. For the first upload, batches work best — `PUT /v1/members`. 3. **Feed.** Show people from the network alongside your own: `GET /v1/members/{id}/feed`. Each card shows which app the person is from. In a swipe app, a skip without a like is a dislike. On a search site, pages use the cursor, and a dislike is only an explicit rejection. 4. **Likes, matches, messages** — a few requests plus receiving events: through webhooks or with `GET /v1/events`. The effort depends on how your product is built. The minimum user flow includes profiles and consent, the feed, likes and skips, messaging, and event handling. ## Network principles - **The user is yours.** The network does not talk to your users directly, does not collect their contacts and does not poach them. The whole interface is yours. - **Consent only.** The network includes only people who turned on participation themselves. Chatting in other languages is a separate consent. - **Immediate deletion.** One request erases a person's profile from the network; the people they talked to keep the chat history without the name and photos. - **Pause the connection.** Suspension hides the platform's profiles from discovery and prevents new likes and messages in both directions. History is not deleted and event delivery is paused. This is not deletion of user data; profile deletion uses a separate request. - **Free basic integration.** Boost, superlike, incognito and rewind are optional [network services](/en/docs/services/), starting at zero RUB. Platforms set their own user-facing offers. Rules against fake profiles and harvesting apply to everyone; no ordinary-growth tariff is set. - **Open protocol.** The specification is public; changes are versioned and announced in advance, breaking ones 60 days ahead. ## Protocol version This documentation pins specification **1.0.0** from the agreed network release. It does not confirm that this version is already deployed on the public API: check `GET /v1/capabilities` with the target environment's key before enabling features. A `404` means this runtime contract is not supported yet, not that every feature is available. The protocol includes profiles and consent, nearby and worldwide feeds, likes, super likes, dislikes, matches, and events by webhook or by request. Translation depends on a configured service; failures leave the original available. Production video is disabled until frame moderation exists: when `videos: false`, do not submit nonempty `videos`. Capabilities describe configuration, not external service health; `/readyz` only checks database availability. There are no calls yet. The full list is in the [changelog](/en/docs/changelog/). # Test the API in the sandbox To test the API, you need a test key and `curl`. You’ll submit a test profile, fetch the sandbox feed and test a like, a match, a message and an event. You can receive events via webhook or pull them from the API (step 5). See the [integration overview](/en/docs/#from-application-to-launch) for the full path to launch. ## 1. Get your keys [Apply](/en/apply/). We review every application by hand and then open the [dashboard](/en/cabinet/) for you — you log in with a code sent to the email you applied with. Keys are not sent by email: your platform issues them in the dashboard itself. The dashboard has two environments — test and live — each with its own values. Start with test: issue a key and set your webhook URL. The key is shown only once — save it right away. | What | Example | Where to store it | |---|---|---| | `partner_id` | `d097d65b-…` | anywhere | | `api_key` | `dn_3f9c…` | **server only**, in secrets | | `webhook_secret` | `whsec_8a1e…` | on the server — to verify webhook signatures | Never put the API key in a mobile app or in JavaScript on a web page: all requests to the network go from your server. ```bash export PEREVAL_API_KEY=dn_... ``` ## 2. Send a profile The `external_id` in the path is **your** user id, any string. The network will refer to this person by the same id in all responses and events. ```bash curl -X PUT https://api.pereval.dating/v1/members/user-123 \ -H "Authorization: Bearer $PEREVAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "consent": {"network": true, "cross_language": true, "given_at": "2026-10-24T12:00:00Z", "text_version": "2026-10-24"}, "display_name": "Anna", "birth_year": 1994, "gender": "female", "age_min": 28, "age_max": 40, "languages": ["ru"], "city": {"name": "Moscow", "country": "RU", "lat": 55.75, "lon": 37.62}, "bio": "Mountains, jazz and good coffee", "photos": ["https://cdn.your-app.ru/u/123/1.jpg"], "intents": ["serious", "casual"] }' ``` Response: ```json { "network_id": "6f1c2b4e-8a3d-4c55-9b1e-2f0a7d9c1e44" } ``` A repeated `PUT` with the same `external_id` updates the profile — call it on every profile change. If you already have many users who turned participation on, send them in batches of up to 100 — see [first upload](/en/docs/members/#first-upload). ## 3. Get the feed ```bash curl https://api.pereval.dating/v1/members/user-123/feed?limit=20 \ -H "Authorization: Bearer $PEREVAL_API_KEY" ``` ```json { "items": [ { "network_id": "1b9e…", "partner": { "id": "…", "name": "Dating Plus" }, "display_name": "Elena", "age": 31, "gender": "female", "city": "Moscow", "distance_km": 4, "languages": ["en"], "bio": "Люблю горы и кофе", "bio_original": "I love mountains and coffee", "bio_translated": true, "photos": ["https://…"], "photo_verified": true, "verified_by": "partner", "intents": ["serious", "casual", "friendship"] } ], "remaining_today": 180, "next_cursor": "eyJ…" } ``` Show these profiles together with your own. Mark the profile as coming from the Pereval network, and if `bio_translated` is set, add an "AI-translated" label and a way to see the original. The response may include `next_cursor`. The next page is the same request with `cursor`. Without a cursor, a repeat returns the same people. This minimum is one page. If this is a swipe app and the person skipped a profile without a like, send a dislike — the network hides it for 30 days. A search site does not do that: pages there use the cursor. Both paths are written out separately — [swipe app](/en/docs/guide-app/) and [search site](/en/docs/guide-site/). ```bash curl -X POST https://api.pereval.dating/v1/dislikes \ -H "Authorization: Bearer $PEREVAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "user-123", "to": "5c2a…"}' ``` A member gets up to 200 distinct people a day; how many new ones are left is in `remaining_today`. Showing the same profiles again does not decrease it. ## 4. Send a like ```bash curl -X POST https://api.pereval.dating/v1/likes \ -H "Authorization: Bearer $PEREVAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "user-123", "to": "1b9e…"}' ``` `{"matched": false}` means the like is saved, and Elena's app will receive a `like.received` event. If she likes back, both apps will receive `match.created`, and the response to the like will be `{"matched": true, "match_id": "…"}`. ## 5. Receive an event The network sends events as `POST` requests to your webhook URL. A minimal handler in PHP: ```php # Start: swipe app For an app where a person opens one profile, likes or skips it, and continues through the deck. A site with filters and pages of profiles is a different start: [search site](/en/docs/guide-site/). The API key lives only on your server. The app talks to your server, and the server talks to `https://api.pereval.dating`. ## What to store | Field | Why | |---|---| | `network_id` | returned by `PUT /v1/members/{your id}` | | `match_id` | a conversation with someone from the network, from `match.created` | | `next_after` | if you pull events yourself instead of using a webhook | `external_id` in requests is your user's id, a string. ## Consent and profile A "Show me in the Pereval network" toggle, off by default, with a link to pereval.dating beside it. Consent texts are in [Members and consent](/en/docs/members/). Turned on or the profile changed — `PUT /v1/members/{your id}`. Turned off — the same `PUT` with `consent.network: false`. Erased from the network or the account deleted — `DELETE /v1/members/{your id}`. Do not request the network feed until the person has turned participation on. ## The deck ```bash curl "https://api.pereval.dating/v1/members/user-123/feed?limit=20" \ -H "Authorization: Bearer $PEREVAL_API_KEY" ``` Mark the profile as coming from the Pereval network. If `bio_translated` is set, add an "AI-translated" label and the original from `bio_original`. If the card has `videos`, play `url` and show `poster` until then. Clips start silent; one switch turns sound on for all of them, not a control on each clip. An app that does not play video shows `poster` as another photo. A skip without a like is a dislike. The profile stays hidden for 30 days: ```bash curl -X POST https://api.pereval.dating/v1/dislikes \ -H "Authorization: Bearer $PEREVAL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "user-123", "to": ""}' ``` The next feed request without a cursor returns the same people who have not been liked or disliked yet. To take the next batch without marking a skip, pass `cursor` from `next_cursor`. For a swipe deck the usual path is a dislike on skip. One person sees at most 200 new people from the network a day. The remainder is `remaining_today`. Showing the same profiles again does not decrease it. ## Like and chat `POST /v1/likes` with `from` set to your id and `to` set to `network_id`. A response of `matched: true` includes `match_id` immediately. Otherwise the match arrives as `match.created` when the other person likes back. A message is `POST /v1/matches/{match_id}/messages`. Events (`like.received`, `match.created`, `message.created`) arrive by webhook, or you pull them with `GET /v1/events`. Signature and duplicates are in [Events and webhooks](/en/docs/webhooks/). There is no message without a mutual like. ## Done when - [ ] The key is only on the server, and participation is off by default. - [ ] The profile is sent when participation is turned on and when it is edited, hidden when it is turned off, and erased when it is deleted. - [ ] The deck contains people from other apps, and the card says where they are from. - [ ] A skip is a dislike, a like is `POST /v1/likes`, and a match opens a chat. - [ ] Events are not applied twice: the webhook checks the signature over the raw body, or you store `next_after`. - [ ] A translated bio is labeled, and the original can be opened. Optional after that: super like, Worldwide, photo verification. Those are in [feed and matches](/en/docs/feed-matches/) and [members](/en/docs/members/). Before the live key, use the [test environment](/en/docs/testing/). # Start: search site For a site where a person sets filters and opens a page of profiles. Turning the page is not a rejection. A one-by-one deck is the [swipe app](/en/docs/guide-app/) start. The API key lives only on your server. ## What to store | Field | Why | |---|---| | `network_id` | the `PUT /v1/members/{your id}` response | | `match_id` | a conversation with someone from the network | | `next_after` | if you pull events yourself | | the viewer's filters | age, intent, language, height, radius — on your side, sent with every feed request | Filters in the query do not rewrite the profile. The profile still decides whom the person can see. The query can only narrow that. ## Consent and profile Same as a swipe app: the toggle is off by default, `PUT` when it is turned on and when the profile changes, `consent.network: false` when it is turned off, `DELETE` on erasure. Texts are in [Members and consent](/en/docs/members/). Age on the profile (`age_min`, `age_max`) is a mutual range. A search filter does not widen it. ## A page of results ```bash curl "https://api.pereval.dating/v1/members/user-123/feed?limit=20&age_min=28&age_max=35&intent=serious&language=ru" \ -H "Authorization: Bearer $PEREVAL_API_KEY" ``` | Parameter | What happens | |---|---| | `age_min`, `age_max` | The intersection with the profile range. An empty intersection is an empty page and `200`, not an error. `age_min` greater than `age_max` in the query is `422` | | `intent` | One intent from the viewer's profile: `serious`, `casual`, `friendship`, `arrangement`. An intent they don't have is `422` | | `language` | An ISO 639-1 code, two letters. A shared language, or both consents to other languages, is still required | | `gender` | Only the opposite of the viewer. The viewer's own gender is `422` | | `radius_km`, `height_min`, `height_max`, `verified_only`, `lat`, `lon` | As in the [feed](/en/docs/feed-matches/#filters) | Mark the profile as coming from the Pereval network. Label a translated bio and offer the original. `videos[].poster` is the clip's still: show it without downloading the file. Playback from `url` is optional and silent until the visitor turns sound on for every clip at once. ## The next page The response includes `next_cursor` while the page may not be the last. The next request uses the same filters and `cursor=`. No cursor in the response means the pages are over. A bad `cursor` is `422`. Repeating the same request without a cursor returns the same people and does not spend the limit. A dislike is not part of this flow. It hides a person for 30 days and is only for an explicit rejection ("don't show again"). Scrolling and "next page" are not dislikes. ## The limit 200 distinct people a day (UTC) per user of yours, shared with the Worldwide feed if you have one. How many new people can still be shown is `remaining_today`. People already shown today come back and do not decrease the remainder. At zero, new people stop and people already shown today remain, so the grid can refresh. The network does not accept a message without a mutual like. A like from the card is `POST /v1/likes`. The match and the chat then work as in the [swipe app](/en/docs/guide-app/#like-and-chat). ## Done when - [ ] Filters on the site go into the feed request and are no wider than the viewer's profile. - [ ] Pages follow `next_cursor`. Repeating a page does not decrease `remaining_today`. - [ ] Scrolling does not send a dislike. A dislike exists only for an explicit rejection. - [ ] At zero, new people do not appear, and people already opened today open again. - [ ] Like, match and messages work. There is no message without a match. - [ ] The card shows the person's app. A translation is labeled. Your own profiles are not in this feed: you show them with your own search. Before production, use the [test environment](/en/docs/testing/). # Start: bot For a bot that has no public URL for webhooks. The bot pulls events itself. If you do have a URL and want push delivery, webhook handling is in [events](/en/docs/webhooks/), and the deck is in the [swipe app](/en/docs/guide-app/) start. The API key stays on the bot's server, not in the messenger client. ## What to store | Field | Why | |---|---| | `network_id` | the response to `PUT /v1/members/{id in the bot}` | | `match_id` | which bot chat corresponds to the match | | `next_after` | the number from the last `GET /v1/events`. The next pull starts there. Without it, events come from the beginning | ## Profile The person explicitly turns on "Show me in the Pereval network" inside the bot. Until they do, send nothing to the network. Turned on — `PUT /v1/members/{id}`. Consent texts are in [members](/en/docs/members/). Turned off — `PUT` with `consent.network: false`. Deleted — `DELETE`. ## Cards Each time the person opens the deck: ```bash curl "https://api.pereval.dating/v1/members/user-123/feed?limit=1" \ -H "Authorization: Bearer $PEREVAL_API_KEY" ``` Buttons: "like" and "next". "Next" is `POST /v1/dislikes`, and the profile stays hidden for 30 days. "Like" is `POST /v1/likes`. The card caption says which app the person is from, and labels the translation when `bio_translated` is set. `limit=1` returns one profile. The next one, after a dislike or a like, comes from a new request. `remaining_today` is how many new people can still be shown today, at most 200. ## Events without a webhook Leave the webhook URL empty in the dashboard. On a schedule, every few seconds or on any user message: ```bash curl "https://api.pereval.dating/v1/events?after=$NEXT_AFTER&limit=100" \ -H "Authorization: Bearer $PEREVAL_API_KEY" ``` The response has `items` and `next_after`. Store `next_after` before you finish handling them: a repeat with the same `after` returns the same events, and that is normal. Skip an event whose `id` you have already seen. What to show the person: | Event | In the bot | |---|---| | `like.received` | "Someone liked you", a card, and buttons to reply or hide | | `match.created` | open a chat and store `match_id` | | `message.created` | the message. If `translated`, label the translation. If `suspicious`, warn using `flags` | | `message.read` | "read" | | `match.closed` | close the chat. With `peer_deleted`, drop the name and photos | | `match.erased` | delete the chat on your side | | `ping` | nothing for the user. This is how the dashboard checks that pulling works | A message can be sent to the network only inside a match: `POST /v1/matches/{match_id}/messages` with `from` set to the id in the bot. ## Done when - [ ] Without consent turned on, the profile is not sent to the network. - [ ] The card shows a person from another app, "next" sends a dislike, and "like" sends a like. - [ ] `next_after` is stored. A repeated pull does not create a second chat for the same `match_id`. - [ ] An incoming message appears in the bot. A reply goes to the network only when there is a match. - [ ] The person can see a translation as a translation and a suspicious message as a warning. To check this without a second bot, use the [sandbox](/en/docs/testing/) in the test environment: it likes and replies on its own. # How the network works ## Who is who | Term | What it is | |---|---| | **Platform** | A dating app, website or bot connected to the network. In the API, a platform is called `partner` | | **Environment** | Each platform has two: test and live. Each has its own API key, signing secret and webhook URL. The test environment sees only other test platforms and the [sandbox](/en/docs/testing/) | | **Member** | A platform's user who turned on "show me in the network". The platform knows them by their `external_id`; the network, by their `network_id` | | **Feed** | Profiles of members from **other** platforms who are a good fit for your user | | **Like, dislike, match** | A like means "I like them"; a dislike means "saw them and didn't like". A mutual like creates a match and opens chat | | **Event** | A notification from the network to your server: a like arrived, a match was created, a new message, a message was read. It arrives as a webhook, or you pull it yourself with `GET /v1/events` | ## How data flows Anna uses your app, and John has an account on another platform. They meet through their own apps. The platform servers call the Pereval network API. 1. **Profiles.** Anna and John opt in to the network. Each platform's server sends its user's profile with `PUT /v1/members/{external_id}`. 2. **Feed.** Anna opens her feed. Your server calls `GET /v1/members/{external_id}/feed` for Anna. If John meets the feed criteria, the network returns his profile. 3. **Like.** Anna likes John. Your server calls `POST /v1/likes`, and the network delivers a `like.received` event to John's platform server. 4. **Match.** John likes Anna back. His server calls `POST /v1/likes`. The network creates a match and delivers a `match.created` event to both servers. 5. **Message.** Anna writes to John. Your server calls `POST /v1/matches/{match_id}/messages`. The network delivers the message to John's platform server in a `message.created` event and adds a translation when needed. ## Who the feed shows A person appears in your user's feed if all of the following are true: - they are of the opposite gender; - each person's age is within the other's range; - they are within 50 km or the viewer's `radius_km` filter (or in the same city and country if there are no coordinates) — in the Worldwide feed distance doesn't matter, but both turned Worldwide on; - they share a language, **or** both agreed to chat in other languages; - they share an intent; - they were active in the last 30 days; - your user has not liked them and has not disliked them in the last 30 days, and neither has blocked the other. Recently active people come first. The network does not return your own users in the feed: you show them yourself, as before. Pages are returned with a `next_cursor`. Repeating the same request returns the same people. A member gets at most 200 distinct people a day: showing them again does not spend the limit. Details are in [Feed, likes, matches](/en/docs/feed-matches/). Which request to send from a swipe app, a search site and a bot is in the [starts](/en/docs/). ## Translation Each member has a primary language — the first one in `languages`. The member speaks every language in `languages`, and the network never translates from them. If a text — a profile bio or a message — is in a language the reader doesn't speak, the network translates it into the reader's primary language and returns the original alongside it: - in the feed and profiles — `bio`, `bio_original`, `bio_translated`; - in messages — `text`, `text_original`, `translated`. Translation runs in the background. A bio is translated into the languages of the people reading it: when a reader with a new primary language appears, they get the original on first view, and the translation is prepared for their next feeds. A message is delivered to the other person once its translation is ready. The network detects the language of the text itself. Short replies ("ok", "hi!") can't be detected reliably, so the language is picked among the author's `languages` — keep them accurate, primary first. Show an "AI-translated" label and let the user see the original. If translation fails, the message is still delivered — in the original, with `translated: false`. Since version 0.5 the network translates into 38 languages: `ru`, `en`, `uk`, `be`, `kk`, `de`, `fr`, `es`, `it`, `pt`, `nl`, `pl`, `cs`, `sk`, `ro`, `hu`, `bg`, `sr`, `hr`, `el`, `tr`, `sv`, `da`, `fi`, `he`, `ar`, `fa`, `hi`, `ja`, `ko`, `zh`, `vi`, `th`, `id`, `ka`, `hy`, `az`, `uz`. `GET /v1/languages` returns the list. Other languages may be listed in `languages` — the person speaks them, but there is no translation into them. ## What the network stores and what it doesn't | Stores | Does not store | |---|---| | The member's profile, while they are in the network | Email, phone number, documents | | The time and version of the consent text | Exact coordinates — rounds them to ~1 km | | Likes and matches | | | Messages within matches — up to 90 days from sending | | | Events — 7 days, dislikes — 30 days | | When a member is removed from the live network database, their profile, likes, dislikes and blocks are erased; the other person's conversation history remains without the name and photos. If an erased chat has a report, a moderation copy is retained in the live database for 90 days from erasure. Separately, reports and personal photo-check materials, including unresolved disputes, are kept for up to 60 days in the live database from original report submission or check creation, then up to 30 days in closed backups. Appeals, updates and decisions do not extend the period. Deleting materials does not approve photos or lift a block; seek review before the 60-day deadline. Turned-off participation is not removal: the person is hidden and their chats go on. All retention periods, separate backup retention and platform obligations are in the [network rules](/en/docs/safety/#network-rules). ## Reliability - Each change and its events are written together — an event is not lost even if your server was unavailable. - Undelivered webhooks are retried for up to 3 days. You can also pull what you missed: `GET /v1/events` returns events from the last 7 days. - All write requests are idempotent wherever possible: a repeated profile `PUT`, a repeated like and a repeated block break nothing. # Integration options For a new integration, the main path is the **server-to-server API**: your server talks to Pereval, and the user interface stays in your product. An OpenAPI client and an AI agent help implement that same API; they are development tools, not separate ways to connect your users. Ready request flows by product type are here: [swipe app](/en/docs/guide-app/), [search site](/en/docs/guide-site/), [bot](/en/docs/guide-bot/). | Option | For | Status | |---|---|---| | **Main approach: server-to-server API + events** | Apps, websites and bots with their own backend | Available | | **Development tool: OpenAPI-generated client** | A typed client for calling the same API | Available | | **Development tool: AI agent** | Help writing an integration from the docs | Available | | **AI-agent tool: MCP** (`POST /mcp`) | The agent calls the network with an API key; not a replacement for user-facing integration | Available | ## Server-to-server API The main and most flexible option. Your server calls the [API](/en/docs/reference/) and receives [events](/en/docs/webhooks/) — through webhooks or with `GET /v1/events`; the app talks only to your server. You fully control the interface: how network profiles look, where the feed appears, how notifications read. What you'll need: 1. Store the `network_id` of your users who are in the network (it is returned in the `PUT` response). 2. A table of conversations with people from the network: `match_id` → your conversation. 3. A webhook handler with signature verification and duplicate protection. Without a webhook URL — regular pulling of events with `GET /v1/events`. 4. In the interface: consent toggles, a "from app X" label, an "AI-translated" label, a warning on suspicious messages. First, [test the API in the sandbox](/en/docs/quickstart/); then build the user flow using the [guide for your product type](/en/docs/). ## OpenAPI-generated client The specification is at [`/openapi.yaml`](/openapi.yaml) (OpenAPI 3.1, including webhooks). You can generate a client from it in almost any language: ```bash npx @openapitools/openapi-generator-cli generate \ -i https://pereval.dating/openapi.yaml \ -g php -o ./pereval-client ``` Instead of `php`, use `python`, `typescript-fetch`, `go`, `java`, `kotlin`, `swift5`, `csharp` and other generators. ## AI agent working from the docs If an AI agent (Claude Code, Cursor, Copilot and similar) writes the integration, give it: - [`/llms-full.txt`](/llms-full.txt) — all documentation and the specification as a single text; - [`/openapi.yaml`](/openapi.yaml) — the exact description of requests and events. A ready-made task prompt for the agent is on the [For AI agents](/en/docs/ai-agents/) page. Every documentation page has a "Copy for AI agent" button. ## MCP — available The same partner key as REST, `POST /mcp`. JSON-RPC 2.0, protocol version `2024-11-05`. Methods: `initialize`, `tools/list`, `tools/call`, `ping`. Tools: `upsert_member`, `get_feed`, `like`, `dislike`, `send_message`, `list_events`, `list_languages`. A tool result is JSON in `result.content[].text`. Do not put the key in client-side code. ## Need help Use the [contact form](/en/contact/) — we'll help you choose a path, review your data model and answer integration questions. Read replies and continue the conversation on this site using the private link you must save after submitting. Incoming email is not accepted. # Optional network services Basic integration, ordinary likes, matches and chat do not require buying additional services. A platform can join and use the network without them. The network provides services **to platforms** for their users; it does not collect payment directly from people. Platforms set their own user-facing offers and prices. All four services start at **0 RUB**. Even a zero-price order requires confirmation and a separate spending credential, and creates an entitlement. This lets you test integration before nonzero pricing. TEST credits are not money and cannot be used for live checkout. Positive RUB payments are not supported in the current release. ## Available services | Service | Unit | Effect | |---|---|---| | Boost (`boost`) | Minute, 1–1440 per order | Additional priority among eligible recipients in supported feeds | | Superlike (`superlike`) | Action, 1–100 per order | A highlighted like before a match, with an optional message up to 200 characters | | Incognito (`incognito-day`) | Day, 1–365 per order | Permission to enable private mode for one member | | Rewind (`rewind-day`) | Day, 1–365 per order | Unlimited eligible undo actions during the period: likes before a match, and dislikes | One day is exactly 24 hours. Entitlements belong to a particular member, identified by your own `external_id`. Each new superlike consumes a unit from the first executed action; the safety ceiling of five per day is not a free allowance. Ordinary likes remain free. Matches do not expire with time, and their lifetime is not sold. Reports, blocks, erasure and leaving the network remain free. ## Explicit enrollment preserves existing integrations First check `GET /v1/capabilities`. `commerce_enabled` means the full services API is enabled, **not that your platform is enrolled**. `commerce_live_zero_only` indicates zero-only RUB checkout. `commerce_pilot` describes the separate TEST-only development pilot; it is false in production. Existing live clients retain legacy superlike/rewind behavior until explicit enrollment. Implement orders, entitlements and durable action references first, then [contact us](/en/contact/). An administrator enrolls the live spending account and delivers a separate `X-Commerce-Key` privately. The dashboard does not yet issue this credential or place orders. Keep it and your ordinary API key on your backend only. Receiving promoted profiles is free and needs no spending account. You do not have to enable all services. ## Quote, confirm, entitlement 1. `GET /v1/commerce/catalog`: current SKUs, currency, unit prices, quantity bounds and `fulfillment_ready`. Live checkout uses RUB; test checkout uses TEST. Do not substitute one for the other. 2. `POST /v1/commerce/quotes`: quote for your member, valid for five minutes. Nothing is charged or activated yet. 3. `POST /v1/commerce/orders`: confirmation with the normal Bearer key and separate `X-Commerce-Key`. `external_order_ref` is your durable order identifier. 4. `GET /v1/members/{external_id}/entitlements`: granted rights; `GET /v1/members/{external_id}/boost`: boost state and remaining time. ```sh # Backend only. Use the quote UUID returned by the first request. curl -X POST 'https://api.pereval.dating/v1/commerce/quotes' \ -H "Authorization: Bearer $PEREVAL_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"member":"user-123","sku":"boost","quantity":40}' curl -X POST 'https://api.pereval.dating/v1/commerce/orders' \ -H "Authorization: Bearer $PEREVAL_API_KEY" \ -H "X-Commerce-Key: $PEREVAL_COMMERCE_KEY" \ -H 'Content-Type: application/json' \ -d '{"quote_id":"","external_order_ref":"boost-order-42"}' ``` After a lost response, retry the same `external_order_ref` and `quote_id`: the same order returns without another activation or extension. Reusing the reference with another quote conflicts. Do not create a fresh order while the previous result is unknown. The quote snapshots the price; a disabled service or expired quote cannot be committed. `GET /v1/commerce/balance` returns TEST credits or a zero-only RUB balance, not a bank account or monetary funding facility. Zero-price orders retain authorization, grants, accounting and replay protection. There is no automatic funding or renewal. ## Boost: activation, delivery and time - A boost starts **immediately after successful order confirmation**, including a zero-price order. Neither audience size nor a first impression is a prerequisite. - Another purchase extends the same active generation without increasing strength or resetting rotation history. A purchase after completion creates a new generation. - Started boosts cannot be voluntarily cancelled or refunded. Hiding or deleting the profile does not stop time. Platforms handle their own user warnings and financial arrangements. - If incognito is already enabled, a new boost fails **before order execution**: `409 boost_incompatible_with_incognito`. Incognito can be enabled during an existing boost: privacy takes effect immediately while time keeps running. - Filters, consent, blocks and moderation take precedence. There is no guarantee of views, likes, matches or results with an empty audience. The receiving platform opts in: ```json PUT /v1/commerce/delivery { "format": "search", "enabled": true } ``` Supported formats are `swipe` and `search`; disable with `{"format":"none","enabled":false}`. A map-based platform may buy outgoing boosts for its users, but receiving promotion on a map is not supported yet. Do not call an ordinary map point a fulfilled boost. Additional promotion occupies at most 20% of a stable API stream (positions 5, 10, 15, and so on), without duplicates. Missing eligible boost candidates fall back to organic profiles. The receiving platform preserves priority in its interface. This is neither an obligation to show ads nor proof that a person viewed a card. Keep `X-Feed-Request-Ref`, parameters and `next_cursor` for one logical search. Retries/pages belong to that search; an explicit new search gets a new reference. Sessions last 15 minutes, up to 200 candidates; changing filters requires a new search. Materialized pages do not count twice, and eligibility is rechecked before responding. The daily feed allowance is shared with the ordinary feed. No per-card impression callbacks are required. `api_placements` means additional API placements, not human views. A detected checkpoint gap greater than ten seconds conservatively pauses the shared promotion clock and resumes the same generation with preserved remaining time. This mechanism does not detect every partial outage or the failure of an individual platform. A small audience is not an outage. ## Superlikes and rewind: action retries After enrolling a spending account, send `X-Action-Ref` for REST super likes and rewinds. The MCP `like` tool also accepts the reference as the `action_ref` argument for a super like. If you send both the argument and the header, their values must match. Reuse the reference only when retrying with the same sender, recipient, action and message. Retries across REST and MCP consume the entitlement once and emit one event. Use REST to place orders and rewind decisions. Superlike execution and entitlement consumption are atomic; failed effects roll back consumption. Its optional message is part of the superlike, not a separate message-before-match product. Rewind needs a currently valid day entitlement, with no commercial operation counter. It cannot undo a match or resurrect erased chat. An expired right yields `402 entitlement_required` on a new rewind. ## Incognito: expiry and privacy `PUT /v1/members/{external_id}/incognito` with `{"enabled":true}` enables the mode with a valid right; `false` disables it free of charge. Buying days grants permission but does not turn the mode on. Subsequent day purchases append nonoverlapping intervals. Turning the mode off does not pause entitlement expiry. Expiry does not automatically publish the profile: privacy stays enabled and new browsing is blocked until renewal or an explicit return to normal mode. Previous disclosures and existing matches cannot be retroactively removed from another platform's cache. Ordinary hiding with `consent.network:false` is free and needs no incognito purchase. ## Errors and events Main responses: `402 entitlement_required`, `403 commerce_forbidden`, `409 commerce_conflict`, `409 quote_expired`, `409 boost_incompatible_with_incognito`, `422 invalid_commerce_request`, `503 commerce_unavailable`, `503 live_payments_unavailable`. TEST credit exhaustion uses `402 insufficient_test_credits`. Resolve the cause instead of blindly replacing the order reference. Boost emits `boost.started`, `boost.extended`, `boost.paused`, `boost.resumed` and `boost.completed` through the normal event queue. Apply ordinary [webhook](/en/docs/webhooks/) signatures and deduplication. Safely ignore unknown event types for compatibility. After restoring an old backup, reconcile newer orders and actions before reopening entitlement issuance; clearing/replaying outbox is not that reconciliation. See the [API reference](/en/docs/reference/#commerce) for fields and examples. Take current prices from the catalog, not documentation text. The network does not require platforms to charge their users, and payment never authorizes fake profiles or harvesting the network database. # Profiles and consents ## Consents — what to show the user Only people who turned on participation themselves are added to the network. Add toggles with short explanations to your profile settings. Below are texts you can use as is; just change the app name. ### "Show me in the Pereval network" — required The toggle is off by default. Next to it, put a link to pereval.dating, which explains what the network is. > **Show me to people from other apps** > Your profile (name, age, city, photos, bio) will be visible to users of other dating apps connected to the Pereval network. You chat with them right here in [Your app]. Your phone number and email are not sent to the network. The network receives coordinates from your app and stores rounded coordinates; other users do not receive coordinates. You can turn participation off at any time — new people from other apps won't see you, and your chats stay. "Delete my data from the network" removes your profile and closes matches, but does not erase the other person's chat history: it remains without your name and photos. Separate copies of messages may be retained to investigate reports. ### "Open to chatting in other languages" — optional > **Meet people who speak other languages** > Messages and profile bios can be translated automatically when the translation service is available. The original is always available; if translation fails, the original is shown. ### "Worldwide" — optional For people who want to meet someone from another city or country. Those who turn it on get the Worldwide feed (`scope=world`, see [Feed](/en/docs/feed-matches/#worldwide)) and appear in it themselves — but only to others who turned it on too. Offer it together with other languages: people from abroad usually speak a different one. > **Meet people from other cities and countries** > Your feed gets a Worldwide tab — people from anywhere, no distance limit. Only people who turned it on too are there, and only they see you. ### "Show distance to me" — optional Off by default. Does not change the profile city. Other people's cards get `distance_km` only if this person turned it on. > **Show distance to me** > Others will see how many kilometres away you are. Your exact address is never shown. Searching another city is a feed `lat`/`lon` parameter, not a city change on the profile (the city can still change only once a day). ### What to send to the network ```json "consent": { "network": true, "cross_language": true, "world": false, "given_at": "2026-10-24T12:00:00Z", "text_version": "2026-10-24" } ``` - `given_at` — when the person actually turned on the toggle (store it on your side). - `text_version` — the version of the text they saw. Using the date the text changed works well. If the text changes substantively, ask for consent again and update the version. - Both values must be real. Uploading a user base without consent gets the platform disconnected — see the [network rules](/en/docs/safety/#network-rules). ## Intents `intents` says what the person is looking for. Two people meet in the feed only if they share an intent. | Value | Intent | |---|---| | `serious` | A serious relationship | | `casual` | Something casual | | `friendship` | Friendship, chatting | | `arrangement` | A relationship with a well-off partner | If omitted, it defaults to `["serious", "casual", "friendship"]`. ### Relationships with a well-off partner A separate, strictly opt-in category. - It's enabled for your app when you join the network. Without that, profiles with `arrangement` are rejected (`422`). - `arrangement_role` is required: `provider` — ready to support a partner; `seeker` — looking for a well-off partner. - If two people share only `arrangement`, they see each other only with opposite roles. - The other person's role is shown only to viewers who are in this category themselves. - **Offering sex for money, price lists and "paid meetings" are forbidden** in every category. A `commercial` report removes the member from the network at once. User-facing text: > **Looking for a relationship with a well-off partner** / **Ready to support a partner** > Only people who chose the opposite option will see you. This is about relationships, not services: offering paid meetings is forbidden and leads to a ban. ## When to send a profile Call `PUT /v1/members/{external_id}`: - when a person turns on participation in the network; - on any change to the profile, photos, preferences, city, languages or consents; - when a person has passed your photo verification — with [`verification`](#photo-verification) or [the network's check](#photo-check-by-the-network). The request is idempotent: sending the same profile again breaks nothing. Save the `network_id` from the response — it lets you recognize your user in other people's feeds and avoid showing them to themselves. The network translates the bio in the background, after the profile is saved. Until the translation is ready, other people's feeds show the original. ## First upload When you connect, you may already have people who turned participation on. Send them in batches: `PUT /v1/members`, up to 100 profiles per request. ```json PUT /v1/members { "items": [ { "external_id": "user-123", "consent": { … }, "display_name": "Anna", … }, { "external_id": "user-124", "consent": { … }, "display_name": "Olga", … } ] } ``` Each profile is validated on its own: one bad profile doesn't block the rest. The response has a result for each one, in the same order: ```json { "items": [ { "external_id": "user-123", "network_id": "6f1c2b4e-…" }, { "external_id": "user-124", "error": { "code": "invalid", "message": "birth_year: members must be 18+" } } ] } ``` Only people who turned participation on themselves: never upload your whole user base. ## Profile requirements | Field | Rule | |---|---| | `display_name` | 1–50 characters. First name only, no last name or contacts | | `birth_year` | Age 18+ | | `gender` | `male` or `female`; the network matches people of the opposite gender | | `age_min`, `age_max` | `18 ≤ age_min ≤ age_max` | | `languages` | ISO 639-1 codes (two letters, otherwise `422`), the first one is primary. The person speaks these languages, so the network never translates them for this person; everything else is translated into the primary one. Translation languages — `GET /v1/languages`, 38 languages | | `city` | Name and country, or coordinates. The network rounds coordinates to ~1 km, but it's better to send the city center right away. The city can change once a day | | `bio` | Up to 1000 characters | | `photos` | 1–9 public `https://` URLs. No watermarks with contacts. The network may cache photos | | `videos` | optional: up to 3 clips next to the photos, not instead of them. Each has `url` (MP4, up to 15 seconds) and `poster` (a still). Omit or `[]` to clear | | `verification` | how and when you checked that the person matches the photos — `method`, `provider`, `verified_at`. Details [below](#photo-verification); or by the network's check: `provider: "pereval"` and `check_id` ([below](#photo-check-by-the-network)) | | `photo_verified` | Before 0.15 — a "verified" flag. Still accepted as a claim with an unspecified method at the time it arrives; send `verification` instead | | `height_cm` | optional: height in centimeters, 120–230. Don't know it — leave it out | | `show_distance` | off by default: other people's cards may show `distance_km` | A validation error returns `422` with the field in `message`, for example `"birth_year: members must be 18+"`. ## Photo verification The "photo verified" badge in other apps' feeds and the `verified_only` filter use the `photo_verified` mark, which **the network computes**. You send a claim — how and when you checked the person: ```json "verification": { "method": "selfie", "provider": "own", "verified_at": "2026-10-20T14:03:00Z" } ``` | Field | Value | |---|---| | `method` | `selfie` — a photo selfie (e.g. with a gesture); `video` — a video selfie; `liveness` — an automated liveness check; `document` — an ID with a face match; `in_person` — met in person; `other` | | `provider` | Who did the check: a vendor (e.g. Sumsub) or `own`. Up to 60 characters | | `verified_at` | When the check passed. Not in the future | The mark counts if: 1. **The network has approved your verification method.** Describe how you verify photos when you connect; the network reviews the method once. Until it is approved, your users' marks don't count in the network. Platforms connected before 0.15 were approved on upgrade. 2. **None of the current photos is newer than the check.** The photos a person joins the network with are covered by a check made before you connected — you don't need to re-verify your users. A photo added after the check switches the mark off until you send a newer `verified_at`. The network recognises the same picture under a new URL by its content. Whether the mark counts and why is in the `PUT /v1/members` response: ```json { "network_id": "…", "photo_verified": false, "verification": "photos_changed" } ``` | `verification` | Meaning | |---|---| | `accepted` | The mark counts | | `none` | No claim | | `no_photos` | No photos | | `partner_not_approved` | The network hasn't approved your verification method yet. Never happens for claims by [the network's check](#photo-check-by-the-network) | | `photos_changed` | A photo is newer than the check: verify the person again and send a newer `verified_at` | The network re-checks a sample of verified people and those with fake signals: "fake" reports, the same photo on another person, generated images. If a platform's marks fail the re-checks, the network stops counting them for all of its users. ## Photo check by the network No verification of your own? Use the network's photo check. It's free. The person takes it on a network page: agrees to the comparison, shows the requested gesture to the camera and takes a picture — with the camera only, not from the gallery. The network compares the picture with each profile photo and tells you the result for each photo. What to do with the result is up to you. You don't need to store pictures or facial data — you never get them. The person's identity is not established from the picture and no facial features are stored. The picture is never shown to anyone: if the check passes, it is deleted right away; if it fails, it is kept for 24 hours so the person can dispute the result; if disputed, until the network moderator decides. The consent on the page names both you and the network: the network compares the photos on your behalf. **1. Create a session** — from your server, with your API key: ```http POST /v1/photo-checks { "ref": "u_123", "lang": "en", "return_url": "https://your.app/verified", "photos": [ { "id": "ph1", "url": "https://cdn.your.app/ph1.jpg" }, { "id": "ph2", "url": "https://cdn.your.app/ph2.jpg" } ] } ``` | Field | Meaning | |---|---| | `ref` | How you will recognise the person in the result, usually their `external_id`. 1–200 characters | | `photos` | 1–9 profile photos — public `https` URLs. `id` is your photo identifier, the position by default. The network downloads the photos right away | | `lang` | Page language: `ru` or `en` | | `return_url` | Optional: an `https` address for the "Back" button at the end of the check | Reply `201` — the session, with the check page `url` and `expires_at`. At most 10 sessions per person a day (`429 photo_check_limit`); the comparison service is unavailable — `503 photo_check_off`, retry later. **2. Open the `url` for the person** — in the browser, a new tab or your app's in-app window (a webview with camera access). The link is valid for 30 minutes and allows up to 5 attempts. The page can't be embedded in an iframe on your site. Besides the event, the page passes the result to the window that opened it: `postMessage({ type: "pereval.photo_check", status })`. **3. The result** comes as a `photo_check.updated` event — after each attempt, when the person disputes the result and when the moderator decides; go by the latest one. The same is in `GET /v1/photo-checks/{id}`: ```json { "id": "…", "ref": "u_123", "status": "partial", "attempts": 1, "checked_at": "2026-10-30T09:14:00Z", "dispute_until": "2026-10-31T09:14:00Z", "photos": [ { "id": "ph1", "url": "https://cdn.your.app/ph1.jpg", "verdict": "match" }, { "id": "ph2", "url": "https://cdn.your.app/ph2.jpg", "verdict": "no_match" } ] } ``` | `status` | Meaning | |---|---| | `created` | The person hasn't taken the check yet | | `passed` | It's them on every photo with a face | | `partial` | Some photos don't look like the picture | | `failed` | Didn't pass; why — in `reason`: `no_match` (no photo looks like them), `no_face`, `many_faces`, `no_gesture` (gesture not recognised), `virtual_camera` | | `disputed` | The person disputed the result; a network moderator decides — usually within 24 hours | | `resolved` | The moderator decided: the per-photo result is theirs, `note` is an explanation for the person | | `expired` | The link expired and the person never took the check | Per-photo `verdict`: `match` — the same person; `no_match` — doesn't look like them; `unsure` — couldn't compare reliably; `no_face` — no face, not counted. **4. The mark in the network.** Once the check has passed (`passed` or `resolved`), send a claim that refers to it: ```json "verification": { "provider": "pereval", "check_id": "…" } ``` The network verifies that the check is yours, has passed and that all current profile photos were in it with a `match` or `no_face` verdict, at least one `match`. A photo is recognised by its URL or its content. Then the mark counts **even if the network hasn't approved your own verification method yet**, and `verified_at` is the time of the check. Otherwise — `422` with the reason. A new photo after the check switches the mark off as before: take the person through the check again. ## Changing the city The city can change once a day. If you send a new city, or a location more than 50 km away, sooner than 24 hours after the previous change, the network returns `409 location_change_limited`. Moving around within the city is not limited. The profile is not updated in that case. Tell the person they can change the city in a day, and send the other changes with the previous city. ## Photos The network shows photos from your URLs. Requirements: - URLs are accessible without authentication and don't expire (or update the profile when the URLs change); - EXIF metadata is stripped from the files — it can contain location; - the first photo is the main one and shows the face clearly. ## Video Production video is disabled until frame moderation exists. Check `GET /v1/capabilities`: when `videos: false`, nonempty `videos` is rejected. The following describes the protocol format, not a promise of availability in the live environment. A short clip on the card, next to the photos. Photos stay required: face checks use them, and an app that does not play video still has a card. ```json "videos": [ { "url": "https://cdn.example/u/1.mp4", "poster": "https://cdn.example/u/1.jpg" } ] ``` - Up to three clips. A repeated `url` is `422`. - `url` is a public MP4, at most 15 seconds and 20 MB. The file stays on your host; the feed carries the link. - `poster` is a still. Apps show it before playback, and apps that cannot play video show it instead. - The protocol has no mute flag. A clip starts silent. One switch in the viewing app turns sound on for every clip, not a flag on each one. - The poster is moderated like a photo. The network downloads the file and reads its duration; it does not decode frames. Not an MP4 — `video_invalid`, longer than 15 seconds — `video_too_long`, could not be downloaded — `video_unavailable`. The clip URL arrives in `video` on `member.rejected`. ## Moderation A new profile, or one with changed photos, clips, name or bio, is checked by the network before it appears in feeds — usually within seconds. Not allowed: - explicit photos; - insults, threats, obscenity, offers of paid meetings; - contact details — phone, link, messenger handle: people chat inside the apps; - photos that cannot be downloaded (after several attempts). If a profile does not pass, you get the `member.rejected` event with the reason: `photo_nudity`, `offensive_text`, `contact_info`, `photo_unavailable`, `video_invalid`, `video_too_long` or `video_unavailable`. A photo or poster URL is in `photo`; a clip URL is in `video`. The profile is hidden and cannot like until you send a corrected one; resending the same profile does not trigger a new check. Photos and text that already passed are not checked again. It's better to check profiles on your side first: it's easier for a person to fix the profile right away than after the network rejects it. ## Activity The feed favors people who were online recently and hides those inactive for more than 30 days. Report activity with `POST /v1/members/{id}/active` — no more than once every few minutes per user. A profile `PUT` also updates activity. With many users, a batch is easier — every few minutes, for everyone who opened the app in that time: ```json POST /v1/activity { "external_ids": ["user-123", "user-124", "user-125"] } ``` Up to 1000 ids per request. The response is `{"updated": 3}`: how many members were found and updated. Unknown ids are skipped. ## Turning off: hide, don't delete When a person turns participation off, send the same profile with `consent.network: false`. The network hides them instead of deleting: - the record and `network_id` stay — matches, conversations, dislikes and blocks are kept; - they are in nobody's feed and get no feed themselves (`409 member_hidden`); - they can only like back someone who already liked them — that is a match right away; no super likes (`409 member_hidden`); - conversations in existing matches go on both ways; - photos are optional while hidden — they may have deleted them all. Turned it on again — send `consent.network: true`: the person is visible again, with their whole history. Turning it on and off any number of times never creates new profiles. When a hidden person replies to someone from the network (writes in a conversation or likes back), offer to come back: "Show you in the network again?" with "Reply and rejoin the network" and "Just reply". Rejoin only on that tap: it is consent to show the profile again. ## Deferred account deletion If your app gives 30 days to restore an account, tell the network that the person is "deleting": ``` POST /v1/members/{external_id}/deactivate ``` The profile disappears from feeds and "liked you", matches are frozen: the other people's platforms get `match.frozen` — show "Account deleted", messaging is blocked (`409 match_frozen`). Nothing is erased. They came back — `POST /v1/members/{external_id}/restore`: everything is as it was, the others get `match.unfrozen`. Didn't come back within 30 days — delete them (`DELETE` below); if you don't, the network erases them itself: personal data must be destroyed no later than 30 days after consent is withdrawn. ## Deletion Call `DELETE /v1/members/{external_id}?reason=account_deleted` when a person deletes their account with you; `?reason=left_network` (the default) when they tap "Delete my data from the network". The network: - closes their matches: the other side receives `match.closed` with that `reason` and `peer_deleted: true`; - the conversations stay with the other people, like in Telegram — show them without the name and photo: "Account deleted" / "Left the network"; - erases the profile (name, photos, bio, city), likes, dislikes and blocks. If the person turns participation on again, it will be a new profile without the old matches. # Feed, likes, matches ## Feed `GET /v1/members/{external_id}/feed?limit=20` returns up to 50 profiles of people from other apps who are a good fit for your user (the rules are in [How the network works](/en/docs/concepts/#who-the-feed-shows)). ### How to mix with your own profiles Alternating works well: your profile, a network profile, yours, network. This way the user sees more people nearby, and your own users don't get lost. ### What to show on the card Mark the profile as coming from the Pereval network. Use a “From the Pereval network” label or a network badge with an explanation. | Field | How to show it | |---|---| | `bio_translated` | An "AI-translated" label and a "show original" link (`bio_original`) | | `photo_verified` | A verification badge — the same as for your verified users. the network computes the mark: the person's app has an approved verification method and the photos are not newer than the check. `verified_by: partner` — their app verified them | | `distance_km` | "4 km", if present: only for people who turned on `show_distance` | | `country` | Country (ISO 3166-1 code) — in Worldwide, instead of the distance: "Rome, Italy" | | `height_cm` | Height: "178 cm", if the person specified it | ### Worldwide The feed takes `scope`: `nearby` (default) or `world`. Worldwide ignores distance: people from any city and country, but only those who turned on the [Worldwide consent](/en/docs/members/#worldwide--optional) (`consent.world`). The viewer needs it too — otherwise `409 world_consent_required`. The other conditions — a shared language or both open to other languages, a shared intent, age — stay the same. Show the two feeds with a "Nearby | Worldwide" switch rather than mixing them: someone looking for a date in their own city would drown in people from the other side of the world. A member with Worldwide still appears in regular nearby feeds. The 200 a day limit is shared by both feeds. ### Filters the feed takes the viewer's filters — store them on your side and send them with every request: | Parameter | What it does | |---|---| | `radius_km` | The nearby radius, 1–300 km, 50 by default. Ignored with `scope=world` | | `verified_only=true` | Only profiles with `photo_verified` — the mark the network computes | | `height_min`, `height_max` | Height range, 120–230 cm. People who didn't specify their height pass — otherwise a height filter would empty the feed | | `lat`, `lon` | Nearby search centre (both required). Does not change the viewer's profile city | | `city`, `country` | Place name for the same-city fallback when a profile has no coordinates | | `age_min`, `age_max` | Narrows age. The intersection with the viewer's profile range is used. An empty intersection is an empty page and `200`. `age_min` greater than `age_max` in the query is `422` | | `intent` | One intent from the viewer's profile. Only people who have it remain. An intent the viewer does not have is `422` | | `language` | An ISO 639-1 code. The person in the feed must speak it. A shared language, or both consents to other languages, still applies | | `gender` | Only the opposite of the viewer. The viewer's own gender is `422` | A filter does not widen the profile. Invalid values return `422`. If there are few profiles, consider widening `radius_km` yourself and telling the person: "If there are few profiles, the radius grows a little". ### Pages and the daily limit The response includes `next_cursor` while the page may not be the last. The next request uses the same parameters and `cursor`. No cursor in the response means the pages are over. Repeating the request without `cursor` returns the same people. A member gets at most 200 distinct people a day. Showing the same profiles again does not spend the limit. How many new people can still be shown is `remaining_today`: ```json { "items": [ … ], "remaining_today": 180, "next_cursor": "…" } ``` Once the limit is used up, no new people appear until the next day (UTC), and people already shown today still come back. A dislike hides a profile for 30 days. A swipe app uses it for a skip. A search site does not use it to turn the page: pages use the cursor. Both starts are [swipe app](/en/docs/guide-app/) and [search site](/en/docs/guide-site/). ## Like ```json POST /v1/likes { "from": "user-123", "to": "1b9e…" } ``` - `{"matched": false}` — the like is saved, and the other side received `like.received`. - `{"matched": true, "match_id": "…"}` — the like was mutual, and a match was created. Both platforms receive `match.created`. You can't like a user of your own app through the network (`403`) — do that in your app as usual. - You can only like someone your user could see. A block either way, the other environment (test vs live), or a hidden person who hasn't liked your user → `404`, as if the profile didn't exist. - A repeated like changes nothing and sends no event. Upgrading a regular like to a super like sends `like.received` again, now with `super`. - While your user's profile is in the network's moderation, their likes are stored, but the other side gets `like.received` only after the profile passes. If it's rejected, the likes already shown are withdrawn (`like.withdrawn` with `rejected`). ### Super like After explicit spending-account enrollment, a `superlike` order and `X-Action-Ref` are required even at zero RUB. Existing clients keep the legacy contract until enrollment. Five per day is a safety ceiling, not a free allowance. See [network services](/en/docs/services/). ```json POST /v1/likes { "from": "user-123", "to": "1b9e…", "super": true, "message": "Hi! Into hiking?" } ``` A special like the recipient sees right away, without a match. The message is optional, up to 200 characters, no contact details (phone, link, messenger handle — `422`). - The recipient's platform gets `like.received` with `super: true`, and the sender's profile carries `super_like`: the message in the recipient's language (`message`), the original (`message_original`), `translated` and scam signals (`flags`). - In the recipient's feed and incoming likes this person comes first, with the same `super_like`. Show a super like clearly highlighted — the sender used a special action on it. - When a match happens, the super like message becomes the first message of the chat: the recipient's platform gets it as a regular `message.created`, the sender's platform shows it on its side itself. - At most 5 super likes a day per member — then `429 super_like_limit`. A regular like upgraded to a super like counts too. How many your users get and whether they pay for them is up to you. ### Changed my mind After spending-account enrollment, a valid `rewind-day` and `X-Action-Ref` are required. Eligible rewinds have no commercial operation quota during the period. Rewind cannot resurrect erased matches or messages. ```json DELETE /v1/likes/1b9e…?from=user-123 ``` Takes a like or super like back before it became a match; after a match — `409 matched` (close the match instead). The recipient's platform gets `like.withdrawn` and removes the like from "Liked you". A skipped profile comes back with `DELETE /v1/dislikes/{network_id}?from=…` — it may appear in the feed again. ## Dislike ```json POST /v1/dislikes { "from": "user-123", "to": "5c2a…" } ``` Send it when the person skipped a profile in a swipe app, or explicitly rejected one on a search site ("don't show again"). Turning a search page and refreshing the grid are not dislikes: those use `cursor`. A disliked profile leaves the user's feed for 30 days. The response is `204`; the other side is not notified. You can send a batch of up to 100 pairs: ```json POST /v1/dislikes { "items": [ { "from": "user-123", "to": "5c2a…" }, { "from": "user-123", "to": "8d4f…" } ] } ``` ## Incoming likes The `like.received` event contains the profile of the person who sent the like (`from`), already in your user's language. Show it under "Liked you". A `POST /v1/likes` in return creates a match. If events were lost, rebuild the list with: ``` GET /v1/members/{external_id}/likes?limit=50 ``` It returns people who liked the member and are waiting for an answer — without mutual likes, dislikes and blocks, newest first: ```json { "items": [ { "from": { "network_id": "…", "display_name": "Elena", … }, "liked_at": "2026-10-24T12:00:00Z" } ] } ``` ## Match `match.created` is sent to **both** platforms — each with its own user in `member` and the other person's profile in `with`. Create a conversation on your side and save the `match_id` ↔ your conversation mapping: messages, read receipts and closure will arrive by `match_id`. If your user sent the like that made it mutual, the `match_id` arrives both in the like response and in the event — create the conversation idempotently (by `match_id`). ## List of matches `GET /v1/members/{external_id}/matches` returns open matches with the other people's profiles. Use it for reconciliation and recovery after a failure; in normal operation, events are enough. ## Parting ways the network supports different ways to part — build your chat menu from them. **Delete the chat for your user only** — the history is hidden for your user, the other person keeps it, the match stays; new messages flow as usual: ```json POST /v1/matches/{match_id}/clear { "as": "user-123" } ``` **Close the match, keeping the history** — both keep the chat, messaging is not possible (`409 match_closed`); the other side receives `match.closed` with `reason: unmatched`: ``` DELETE /v1/matches/{match_id}?by=user-123 ``` **Delete the pair for both** — the match, messages and likes are deleted for both; the other side receives `match.erased` and removes the chat; they won't appear in each other's feeds for `cooldown_days` (0–365, 30 by default). Need "never" — block. ``` DELETE /v1/matches/{match_id}?by=user-123&erase=true&cooldown_days=30 ``` If either of them reported the other, the network keeps a hidden copy of the deleted chat for 90 days to review reports. A match is also closed if one of the two blocks the other (`reason: blocked`) or is removed from the network (`reason: account_deleted` or `left_network`, plus `peer_deleted: true`). In that case your user keeps the history — show "Account deleted" or "Left the network" instead of the name and photo. ## Boost and incognito See [network services](/en/docs/services/) for optional modes, entitlements, privacy restrictions and receiving formats. No per-card impression callbacks are required. # Messages and translation ## Send a message ```json POST /v1/matches/{match_id}/messages { "from": "user-123", "text": "Привет! Как прошли выходные?" } ``` The response is the message as the sender sees it: ```json { "id": "…", "from": "me", "text": "Привет! Как прошли выходные?", "translated": false, "lang": "ru", "created_at": "2026-10-24T12:00:00Z" } ``` Text must be 1 to 4000 characters. Text only: photos and voice messages are not supported in version 0.14. ### New members In their first 7 days in the network, a member can start at most 20 conversations a day. Beyond that, the network returns `429 first_messages_limit`. Replying in conversations that have already started is unlimited. Tell the person they can start new conversations tomorrow. ## Receive a message The other person's platform receives a `message.created` event once the translation is ready: ```json { "id": "evt_…", "type": "message.created", "data": { "match_id": "…", "to": "john-77", "message": { "id": "…", "from": "6f1c2b4e-…", "text": "Hi! How was your weekend?", "text_original": "Привет! Как прошли выходные?", "translated": true, "lang": "en", "created_at": "2026-10-24T12:00:00Z", "suspicious": false, "flags": [] } } } ``` ## How translation works 1. The network detects the message language; short replies are matched against the author's `languages`. 2. If the recipient doesn't speak it (it's not in the recipient's `languages`), the network translates it into the recipient's primary language — the first in `languages` — preserving tone, emoji and style. 3. The recipient sees the translation, and the original on tap. Translation runs in the background: `message.created` goes to the other person once the translation is ready, so the message arrives already translated. If translation fails, the message is still delivered — in the original, with `translated: false`. Matches across languages are possible only if both members turned on "open to chatting in other languages". For those who haven't, the network doesn't show people without a shared language. ### How to display a translation - An "AI-translated" label under the message. - Tap or long press shows the original (`text_original`). - Nothing needs to be translated in the input field: the user writes in their own language. ## Scam signals The network checks every message for simple signs of fraud: | Flag | What it means | |---|---| | `link` | A link early in the conversation | | `phone` | A phone number early in the conversation | | `messenger` | An invitation to another messenger early in the conversation | | `money` | Talk about money | | `mass_text` | The same text in several other matches within a day — only together with another flag: identical greeting templates are not flagged | The message is delivered anyway, with `suspicious: true` and a list of `flags`: ```json "message": { "id": "…", "text": "Text me on Telegram, it's easier", "suspicious": true, "flags": ["messenger"], … } ``` Show the recipient a warning next to the message. For example: "This person is asking you to move to another messenger. Scammers often do this — don't send money and don't open unfamiliar links." Only messages from the other side carry signals; your user's own messages never do. ## Read receipts ```json POST /v1/matches/{match_id}/read { "as": "user-123", "up_to": "" } ``` The member has read the conversation up to and including this message. Take the `id` from `message.created` or the history. The response is `204`. The other person's platform receives a `message.read` event with `up_to` and `read_at` and can show "read" to its user. ## History ``` GET /v1/matches/{match_id}/messages?as=user-123&limit=50 ``` Newest first. The other person's messages are in the language of `as`, with the original and scam signals. Your user's own messages carry `read_at` once the other person has read them. For older messages, pass `before=`. History is for recovery after a failure or for new devices. The network retains ordinary messages for up to 90 days from sending; store the full history on your side based on events. If an erased chat has a report, a separate hidden moderation copy is retained for 90 days from erasure; it is not user-accessible history. # Events and webhooks The network reports events with a `POST` request to the webhook URL you set in the dashboard. No URL? Pull events yourself — see [below](#pull-events-yourself). Each event is JSON like this: ```json { "id": "evt_6f1c2b4e-8a3d-4c55-9b1e-2f0a7d9c1e44", "type": "match.created", "created_at": "2026-10-24T12:00:00Z", "data": { "…": "…" } } ``` Headers: | Header | Value | |---|---| | `X-Network-Signature` | `sha256=` — HMAC-SHA256 of the **raw body** with your environment's signing secret (`webhook_secret`) | | `X-Network-Event` | The event type, same as `type` | | `Content-Type` | `application/json` | ## All events | Type | When | What to do | |---|---|---| | `like.received` | A person from another app liked your user (`super: true` — a super like, message in `from.super_like`) | Show it under "Liked you"; a super like first and highlighted | | `like.withdrawn` | The person took their like back before a match or is no longer available (`reason`: deleting their account, hidden after reports, profile rejected by moderation, removed). If they come back, `like.received` comes again | Remove it from "Liked you" | | `match.created` | A mutual like | Create a conversation, save the `match_id` | | `message.created` | A new message for your user, already translated | Show it, send a push notification; with a warning if `suspicious: true` | | `message.read` | The other person read your user's messages | Show "read" | | `match.closed` | The other person closed the match, blocked your user or was removed from the network (`peer_deleted: true`) | Close the conversation, the history stays; with `peer_deleted` show "Account deleted" / "Left the network" instead of the name and photo | | `match.frozen` | the other person is deleting their account (`reason: account_deleting`, can restore within 30 days) or was hidden by the network after reports (`reason: suspended`) | Show "Account deleted" or "Account unavailable", no messaging | | `match.unfrozen` | the other person restored their account or was reinstated after reports | Bring back their name, photo and the chat | | `match.erased` | the other person deleted the pair or blocked with erase | Delete the chat on your side | | `member.reported` | Someone reported your user | Review it under your own rules | | `member.suspended` | The network hid your user after reports pending review; their matches are frozen | Review the reports: bring them back (`reinstate`) or block them in your app | | `member.rejected` | The profile did not pass the network's moderation: `reason`, `photo` when a photo or poster is the problem, `video` when a clip is | Ask the user to fix the profile and send it again | | `photo_check.updated` | a new result of [the network's photo check](/en/docs/members/#photo-check-by-the-network) — after an attempt, a dispute or the moderator's decision | Show the person the result; if passed, send a `verification` claim with `check_id` | | `ping` | You clicked "Send a test event" in the dashboard | Reply `2xx` — this checks the URL and the signature | The fields of each event are in the [API reference](/en/docs/reference/#events). ## Signature verification Required: without it, anyone could send you a "message" on behalf of the network. Compute the HMAC over the **raw request body** — before parsing the JSON — and compare in constant time. ### PHP ```php { const expected = "sha256=" + crypto.createHmac("sha256", process.env.PEREVAL_WEBHOOK_SECRET).update(req.body).digest("hex"); const got = req.get("X-Network-Signature") ?? ""; if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) { return res.sendStatus(401); } const event = JSON.parse(req.body.toString("utf8")); handle(event); res.sendStatus(204); }); ``` ### Go ```go func perevalHook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil { http.Error(w, "bad body", http.StatusBadRequest) return } mac := hmac.New(sha256.New, []byte(os.Getenv("PEREVAL_WEBHOOK_SECRET"))) mac.Write(body) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Network-Signature"))) { http.Error(w, "bad signature", http.StatusUnauthorized) return } var event struct { ID string `json:"id"` Type string `json:"type"` Data json.RawMessage `json:"data"` } _ = json.Unmarshal(body, &event) handle(event) w.WriteHeader(http.StatusNoContent) } ``` ## Retries and duplicates - A **2xx** response means the event was delivered. Respond quickly; do long-running work in the background. - Any other response, or no response within 10 seconds, triggers a retry after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h, then once a day, for up to 3 days in total. - The same event can arrive twice. Store processed `id`s and skip duplicates: ```sql CREATE TABLE pereval_events (id text PRIMARY KEY, received_at timestamptz DEFAULT now()); -- in the handler, in the same transaction as your changes: INSERT INTO pereval_events (id) VALUES ($1) ON CONFLICT DO NOTHING; -- 0 rows → already processed ``` - In the 1.0.0 release contract, a recipient's pending events retain causal `seq` order, including retries. Different recipients progress independently. Expired (7 days) or permanently failed events are not replayed forever: reconcile matches and history against the current network state after an outage. Platform suspension pauses delivery without deleting the outbox or extending normal retention. ## Pull events yourself ``` GET /v1/events?after=&limit=100 ``` Returns your environment's events in `seq` order. It helps in two cases: - your platform has no webhook URL — a bot, a website on simple hosting. Without a URL, the network doesn't push events; it only returns them on request; - you need to recover after an outage. ```json { "items": [ { "seq": 41, "id": "evt_…", "type": "message.created", "created_at": "2026-10-24T12:00:00Z", "data": { "…": "…" } } ], "next_after": 41 } ``` Make the first request with `after=0` and the next one with `after=next_after`. Store `next_after` on your side. `data` is the same as in the webhook of that type. Up to 500 events per request. Pulled events count as delivered. Events are kept for 7 days. For live chat, polling every few seconds works well; the overall limit is 600 requests per minute per platform. ## If your server is unavailable Events are queued and delivered after your server recovers, for up to 3 days. They are kept for 7 days: pull whatever didn't arrive by webhook with `GET /v1/events`. Restore the rest with `GET /v1/members/{id}/matches`, `GET /v1/members/{id}/likes` and `GET /v1/matches/{id}/messages`. ## Boost events `boost.started`, `boost.extended`, `boost.paused`, `boost.resumed` and `boost.completed` use the same signed queue and event-id deduplication. Forward them to the services integration; ignore unknown events without error. Reconcile state with GET /v1/members/{external_id}/boost. These events do not confirm a human impression. [Details](/en/docs/services/). ## Erasure, retries and old backups A successful HTTP response does not prove the network has durably recorded ACK: a crash can cause redelivery. Commit the effect and processed event `id` atomically in your database before responding 2xx. Process-memory deduplication is insufficient. For `match.erased`, save a durable erasure marker by match_id atomically with removing the local chat. It must survive restart and ordinary event-log cleanup. Late `match.created`, `message.created` or `match.unfrozen` must not recreate erased chat. A known erasure is an acknowledged no-op (2xx), not endless 503 retries. Reconcile an unknown match before acting; do not rebuild history from a late event alone. After restoring an old backup, keep chat access closed until current state is reconciled and newer erasures are applied. Seven-day outbox is not a complete deletion journal for a 30-day backup. Net and Via use separate operational journals; this public API does not yet offer a universal deletion-history export for third-party platforms. Platforms need their own durable marker/recovery plan. If continuity cannot be established, keep access closed and contact support. # Safety and rules ## Blocking Blocking applies to specific network profiles. It does not establish a person's identity and cannot guarantee that they will not create another account in another app. ```json POST /v1/blocks { "from": "user-123", "to": "" } ``` After a block, the two people no longer see each other in their feeds for good, and the match between them is closed (`match.closed`, `reason: blocked`); the history stays. With `"erase": true` the match, messages and likes are deleted for both (`match.erased`, `reason: blocked`); if there was a report, the network keeps a hidden copy for 90 days to review it. Blocking must also work in your interface: the "Block" button on a profile and in a conversation with a person from the network must call this request. ## Report ```json POST /v1/reports { "from": "user-123", "to": "", "reason": "scam", "comment": "Asks me to send money" } ``` | `reason` | When | |---|---| | `fake` | Someone else's photos, a made-up profile | | `scam` | Asks for money, sends people to third-party websites, "investments" | | `spam` | Advertising, identical messages sent in bulk | | `offensive` | Insults, harassment, unsolicited explicit content | | `commercial` | Offers sex for money, a price list or "paid meetings" | | `underage` | The person appears to be under 18 | | `other` | Something else — describe it in `comment` | What happens next: 1. The platform where this person is registered receives `member.reported` and reviews it under its own rules. 2. A report with the `underage` or `commercial` reason, or reports from three different people, **immediately hide** the person from the network pending review. Their platform receives `member.suspended`. their matches are frozen: the other people's platforms get `match.frozen` with `reason: suspended` — show "Account unavailable"; neither side can write (`409 match_frozen`). 3. A moderator on their platform reviews the reports. Nothing wrong — `POST /v1/members/{external_id}/reinstate`: the open reports are closed, the person is back in feeds, the matches resume (`match.unfrozen` to the other side). The violation is confirmed — block them in your app; if you remove them from the network (`DELETE`), the other side gets `match.closed`. 4. The network reviews reports on its side and may remove the person from the network. ## Scammers in conversations The network checks messages for simple signs of fraud: links, phone numbers and invitations to other messengers early in a conversation, talk about money, the same text sent to many people. The message is delivered anyway, with `suspicious: true` and a list of `flags` — show the recipient a warning. Details are in [Messages and translation](/en/docs/messages/#scam-signals). New members can start at most 20 conversations a day during their first 7 days (`429 first_messages_limit`). This holds back mass messaging from freshly created profiles. ## Network rules By connecting, a platform accepts these rules. A violation means the environment is suspended and, if not fixed, the platform is disconnected. ### User consent - Only people who turned on participation themselves enter the network. The "Show me in the Pereval network" toggle must be in the app settings, off by default, with a link to pereval.dating next to it explaining what this is. Ready-made texts are in [Profiles and consents](/en/docs/members/). - `consent.given_at` and `consent.text_version` are real: when the person tapped the toggle and which text they saw. Uploading a user base without consent means disconnection. - The network checks this with control profiles: it signs up in the platform's app without turning participation on and watches whether the profile shows up in the network. ### Data - When someone turns off participation — `PUT` the profile with `consent.network: false`: they are hidden, their chats go on. When they erase their data from the network or delete their account — `DELETE /v1/members/{id}` right away (if you give 30 days to restore, `deactivate` first). `DELETE` also for anyone you blocked in your app. - On `match.erased` delete the chat on your side; on `match.closed` close it (the history stays), and with `peer_deleted: true` remove the other person's name and photo. - Access inside your app is on you: each user sees only their own conversations. The network separates platforms from each other, but not users within one platform. - The API key is stored only on the platform's server. ### Content - Translated text is labeled ("AI-translated"), and the original is available. - Network profiles show which app the person is from. - In every category, including relationships with a well-off partner, escort and "sex for money", price lists and hourly rates, selling services and advertising are forbidden. Such profiles and messages are removed and the member is excluded from the network. - Minors are forbidden. Verify age under your own rules; only 18+ in the network. - Fake and generated profiles are forbidden. Don't send profiles to the network that you wouldn't allow in your own app: bots, fakes, sellers of services. Send a photo verification claim (`verification`) only after a real check: the network approves your method when you connect, re-checks a sample of verified people and, if the marks fail, stops accepting them for the whole platform. - Accept reports about your users and respond to `member.reported` within a reasonable time. A platform that keeps supplying fakes or scammers to the network is disconnected. ### Retention in the network | What | How long it's kept | |---|---| | Profile | While the person is in the network | | Messages | Up to 90 days from sending. Platforms keep the full history | | Hidden copy of an erased chat with a report | 90 days from erasure, separately from ordinary message retention | | Reports: personal materials | Up to 60 days in the live database from original submission, then up to 30 days in closed backups | | Photo checks, including unresolved disputes: personal materials | Up to 60 days in the live database from check creation, then up to 30 days in closed backups; deletion does not approve photos or lift a block | | Events | 7 days | | Dislikes | 30 days | ### Pricing Basic integration and exchanging profiles, ordinary likes, matches and messages are free. Boost, superlike, incognito and rewind are optional; all start at zero RUB. See [network services](/en/docs/services/) for terms and enrollment. Platforms decide what to offer users and at what price. Positive RUB payments are not supported yet; we give 60 days' notice of changes to additional-service pricing. The network does not sell data or show ads. Bulk fake-profile uploads to harvest the database are forbidden. Buying services does not bypass consent checks, authenticity requirements, blocks or technical limits. No mandatory fee for ordinary audience growth is set. ## Privacy - Email and phone numbers are not sent to the network. The network receives coordinates from the app and stores rounded coordinates; profiles shown to other users do not contain coordinates. - Messages are retained for up to 90 days from sending. If an erased chat has a report, a hidden moderation copy is retained for 90 days from erasure. - Removal from the network erases the profile, likes, dislikes and blocks and closes chats; the other person's history remains without the name and photos. This does not erase all of another person's chat history. - An expired public link cannot open a photo check or its photos. Reports and personal photo-check materials, including unresolved disputes, are retained for up to 60 days in the live database from original report submission or check creation, then up to 30 days in closed backups. Appeals, updates and decisions do not extend the limit; seek review before the 60-day deadline. Deleting materials does not approve photos or lift a block. The message, hidden archive and event periods above refer to the live database; backups are retained separately. The site's form periods are described separately in the [data processing disclosure](/en/privacy/); the report/check 60-day rule does not replace them. # Errors and limits An error is an HTTP status code and JSON: ```json { "error": "invalid", "message": "birth_year: members must be 18+" } ``` Rely on `error` — the codes are stable. `message` is for people and logs; its text may change. ## Error codes | HTTP | `error` | What it means | What to do | |---|---|---|---| | 400 | `bad_json` | The body is not JSON | Check your serialization | | 400 | `bad_id` | A parameter is not a UUID (`match_id`, `to`, `up_to`) | Pass ids from network responses as is | | 401 | `unauthorized` | Missing key, invalid key, or the platform's environment is suspended | Check the `Authorization: Bearer …` header | | 403 | `forbidden` | The action is not allowed: liking or disliking your own user through the network; the member is hidden after reports or deleting their account (no feed either) | Don't retry | | 404 | `not_found` | No such member, match or message — or it isn't yours. also a like to someone your user can't see: a block, the other environment, a hidden member | The user wasn't sent to the network, was deleted or is unavailable: remove the card | | 409 | `match_closed` | The match is closed | Close the conversation on your side | | 409 | `matched` | The like already made a match — it can't be undone | Don't retry | | 409 | `location_change_limited` | The city was changed less than a day ago: a new city or a move over 50 km | Tell the person they can change the city in a day; send other changes with the previous city | | 409 | `match_frozen` | One of the two is deleting their account (can restore within 30 days) or was hidden by the network after reports | "Account deleted" or "Account unavailable"; messaging resumes after a restore or `reinstate` | | 409 | `member_hidden` | The member is hidden (`consent.network: false`): no feed; can only like back someone who liked them, no super likes | Offer to rejoin the network | | 409 | `photo_check_gone` | Photo check page only: the gesture task expired or was already used | The page offers to start again by itself | | 409 | `world_consent_required` | `scope=world` feed for a member without the Worldwide consent | Offer to turn Worldwide on; until then, the regular feed | | 422 | `invalid` | The data failed validation — the field is in `message` | Fix the data; don't retry as is | | 429 | `rate_limited` | More than 600 requests per minute | Wait and retry with a delay | | 429 | `super_like_limit` | More than 5 super likes a day | Tell the person super likes come back tomorrow; a regular like still works | | 429 | `photo_check_limit` | more than 10 photo checks for one person a day | Tell the person they can try again tomorrow | | 429 | `first_messages_limit` | A new member (first 7 days in the network) has already started 20 conversations today | Don't retry; tell the person they can start new conversations tomorrow. Replies in existing ones are fine | | 503 | `photo_check_off` | the photo comparison service is temporarily unavailable | Retry later | | 500, 503 | `internal` | Network error or temporary unavailability (503 can come even on the key check — your key is fine) | Retry with exponential backoff | ## Retries on your side - **Retryable:** network errors, timeouts, `429 rate_limited`, `5xx`. Delays: 1 s, 2 s, 4 s… up to a minute. - **Safe to retry:** `PUT /v1/members`, `PUT /v1/members/…`, `DELETE /v1/members/…`, `POST /v1/activity`, `POST /v1/likes`, `POST /v1/dislikes`, `POST /v1/blocks`, `POST /v1/matches/{id}/read` — a retry won't create duplicates. - **Careful:** `POST /v1/matches/{id}/messages` — a retry after a timeout may send the message twice. Retry only if you are sure no response arrived, and show the user a "not sent" status. ## Limits | What | Value | |---|---| | Requests | 600 per minute per platform, bursts up to 100 | | Request body | up to 1 MB | | Feed | up to 50 profiles per request, up to 200 distinct people per member per day. Repeating a page does not spend the limit | | Profile batch (`PUT /v1/members`) | up to 100 | | Dislike batch | up to 100 | | Activity marks (`POST /v1/activity`) | up to 1000 per request | | Incoming likes | up to 100 per request | | Events (`GET /v1/events`) | up to 500 per request, kept for 7 days | | Message history | up to 100 per request | | Message | 1–4000 characters | | Profile bio | up to 1000 characters | | Photos | 1–9 per profile | | `external_id` | 1–128 characters | | City change | once a day | | Super likes | 5 a day per member, message up to 200 characters | | New conversations for new members | 20 a day during the first 7 days | | Webhook delivery | 10-second timeout, retries for up to 3 days | If the limits aren't enough for you, write to us and we'll raise them. ## Optional services See [network services](/en/docs/services/#errors-and-events) for `entitlement_required` (402), `commerce_forbidden` (403), `commerce_conflict`, `quote_expired`, `boost_incompatible_with_incognito` (409), `invalid_commerce_request` (422), `commerce_unavailable` and `live_payments_unavailable` (503). `insufficient_test_credits` (402) is TEST-only. Never replace an order reference after a timeout: retry the same request. # For AI agents The Pereval documentation is designed so that an AI agent (Claude Code, Cursor, Copilot, Codex and others) can write the integration. Give it context and a clear task — it will find the rest on its own. ## Context files | File | What's inside | |---|---| | [`/llms.txt`](/llms.txt) | A short map of the documentation with links — following the llms.txt standard | | [`/llms-full.txt`](/llms-full.txt) | All documentation and the specification as a single text — easy to put into context in full | | [`/openapi.yaml`](/openapi.yaml) | OpenAPI 3.1: all requests, fields, constraints, errors and webhooks | | `POST /mcp` | MCP JSON-RPC with the same Bearer key — tools instead of wrapping OpenAPI | Every documentation page has a "Copy for AI agent" button — it copies the page's source text. ## MCP Point the agent at `https://api.pereval.dating/mcp` with `Authorization: Bearer `. After `initialize` (protocol `2024-11-05`) it can `tools/list` / `tools/call`. Same limits and errors as REST; tool failures come back with `isError: true` and a text code (`invalid`, `not_found`, …). To send a super like from an enrolled spending account, call `like` with `super: true` and `action_ref`. Use the same reference and arguments when retrying. Place the order for the super like entitlement through REST. [How orders and retries work](/en/docs/services/). ## Ready-made task prompt Copy it and add a description of your project: ```text Task: connect our dating app to the Pereval network (pereval.dating) as a platform. Context: read https://pereval.dating/llms-full.txt and the specification https://pereval.dating/openapi.yaml. Our stack: . Users are stored in . Do the following: 1. Settings: two consent toggles — "Show me in the Pereval network" (off by default, with a link to pereval.dating) and "Open to chatting in other languages" — with the texts from the "Profiles and consents" section. Store the consent time and version. 2. Profile sync: PUT /v1/members/{our id} when participation is turned on and on any profile change; turned off — the same PUT with consent.network: false (hidden, chats go on); data erased from the network or account deleted — DELETE (or deactivate/restore if we give 30 days to restore). First upload in batches via PUT /v1/members. Save the network_id. On 409 location_change_limited, show the user a clear message. 3. Take the feed from the start that matches the product, not from this list: swipe app — /en/docs/guide-app/ (a skip is a dislike), search site — /en/docs/guide-site/ (pages use cursor, a dislike is only an explicit rejection), bot — /en/docs/guide-bot/ (events via GET /v1/events). Mark the profile as coming from the Pereval network. If the bio was translated, add an "AI-translated" label. The limit is remaining_today, 200 distinct people a day. 4. Likes: POST /v1/likes for network profiles; incoming likes from the like.received event, reconciliation via GET /v1/members/{id}/likes. 5. Conversations: on match.created, create a conversation (idempotently by match_id); messages via POST /v1/matches/{id}/messages and the message.created event, with a warning based on flags when suspicious is true; read receipts via POST /v1/matches/{id}/read and the message.read event; a clear message on 429 first_messages_limit; closing via match.closed (the chat is closed, history stays; with peer_deleted — without the name and photos); match.erased — delete the chat; match.frozen / match.unfrozen — freeze and unfreeze the chat. 6. Activity: POST /v1/activity in batches every few minutes for users who opened the app. 7. Receiving events: a webhook that verifies the X-Network-Signature signature (HMAC-SHA256 of the raw body), protects against duplicates by id and responds with 2xx; 503 if a message arrives before an unknown match; acknowledge known erasures as no-ops and never recreate erased chat. Without a webhook URL, pull GET /v1/events?after= on a schedule and store next_after. 8. Blocking and reporting for network profiles: POST /v1/blocks, POST /v1/reports. On member.suspended — hide the person in our app until the reports are reviewed; if all is well — POST /v1/members/{id}/reinstate; on member.rejected — ask the user to fix the profile. Super likes (super, message) and the Worldwide feed (scope=world, consent.world) are optional. 9. Tests for event handling (signature, duplicates, event order). Do not: store the API key in client-side code. Keys: PEREVAL_API_KEY and PEREVAL_WEBHOOK_SECRET — from environment variables (issued in the platform dashboard). ``` ## Tips - Ask the agent to read [Events and webhooks](/en/docs/webhooks/) and [Errors and limits](/en/docs/errors/) first: the most common mistakes are in signature verification (HMAC over parsed JSON instead of the raw body) and in retries. - For a typed client, have the agent generate it from `openapi.yaml` rather than write it by hand. - Check the result in the test environment with the sandbox — step by step, as described in [Testing](/en/docs/testing/). ## Optional commercial integration Do not enable it without the platform choosing it. Read [network services](/en/docs/services/), check capabilities and implement REST orders/entitlements before explicit spending-account enrollment. Keep X-Commerce-Key on the backend, never in a browser or a general AI agent configuration. Zero pricing still requires authorization and durable external_order_ref/X-Action-Ref. TEST does not convert to RUB. # Testing ## Version and availability The documentation pins the 1.0.0 release contract, not a confirmed production deployment. First check `GET /v1/capabilities` with the target environment's key. Do not enable a feature solely because it appears in OpenAPI. A `404` means the capabilities contract is not supported yet; a missing response does not enable features. `/readyz` checks only the database; capabilities describe configuration, not external service health. Test translation and photo checks separately; do not send clips when `videos: false`. ## Test environment Every platform has two environments in the [dashboard](/en/cabinet/): test and live. Each has its own API key, signing secret, webhook URL and statistics. The API URL is the same — `https://api.pereval.dating`; the key decides the environment. Test profiles are visible only to other test platforms and the network sandbox. They never reach real people, so debug freely. A test webhook URL may also be `http://`, e.g. a tunnel. ## Sandbox The test environment comes with sandbox profiles — "Песочница Перевала" in the `partner` field. They live in Moscow, St Petersburg, Kazan, Yekaterinburg and Novosibirsk, speak Russian or English, male and female. They behave like real people, only faster — within a couple of seconds: - **like a sandbox profile** — it likes you back, you get `match.created`; - **message it** — it replies, you get `message.created`; if the languages differ, the reply is translated; - **send a new profile** in one of these cities — two sandbox profiles nearby like it, you get `like.received`. So you can test the whole flow without a second app of your own: 1. Issue a test key and set your webhook URL. 2. Click "Send a test event" in the dashboard — a `ping` with a valid signature should arrive. 3. Send a profile in one of the sandbox cities — two `like.received` arrive. 4. Get the feed — it shows sandbox profiles. 5. Like one — `match.created` arrives. 6. Message a Russian-speaking profile in English — check the translation and `message.created`. 7. Delete the chat for one side, delete the pair, block, delete the profile — check `match.erased` and `match.closed` (with `peer_deleted`). The dashboard shows a log of recent events and an API request chart: you see what arrived and what went to retry. ## Local webhook development The network must be able to reach your handler. For local development, use a tunnel (ngrok, cloudflared, tuna) and set its URL as the webhook URL of your test environment. You can also do without a tunnel: leave the webhook URL empty and pull events yourself with `GET /v1/events` — see [Events and webhooks](/en/docs/webhooks/#pull-events-yourself). ## Pre-launch checklist - [ ] The API key is stored only on the server. - [ ] Webhook signatures are verified against the raw body; an invalid signature returns `401`. - [ ] A repeated event with the same `id` doesn't duplicate anything. - [ ] The network participation toggle is off by default, with a link to pereval.dating next to it. - [ ] Consents are shown with their texts; time and version are saved. - [ ] Turning off participation sends the profile with `consent.network: false`; erasing data from the network and deleting an account call `DELETE /v1/members/{id}` right away (or `deactivate` if you give time to restore). - [ ] Cards show when a profile comes from the Pereval network. Translated text has an "AI-translated" label and a way to see the original. - [ ] In a swipe app, a skip without a like goes to `POST /v1/dislikes`. On a search site, the page follows `cursor`, and a dislike is only an explicit rejection. - [ ] Messages with `suspicious: true` are shown with a warning. - [ ] `409 location_change_limited` and `429 first_messages_limit` are explained to the user in plain words. - [ ] Blocking and reporting work for network profiles too. - [ ] Photos have no EXIF location data. ## Optional services Test [network services](/en/docs/services/) with a test spending account: quotes/orders, order and action replays, missing/expired entitlements, incognito before/during a boost and integration restart. TEST SKUs use the `-test` suffix; `rewind-day-test` and `incognito-day-test` use days. Test prices are arbitrary. An administrator provisions credits and the separate key; the dashboard does not do this yet. Live checkout uses separate zero-price RUB SKUs, not TEST credits. # API changes This log is for platform developers: new functionality, behavioral fixes and required integration updates. Internal development history preceding the first stable contract is preserved in the repository archive; it is not a list of supported public API versions. Cosmetic website changes do not belong here. ## 1.0.0 — first stable public contract The first stable version brings the existing network functionality into one documented contract. `/v1/...` paths, keys and business request/event payloads are retained; no registration or data migration is required. `GET /v1/capabilities` declares `protocol_version: "1.0.0"`. Check your environment’s deployed version and required capabilities before use. - **Membership and profiles.** Explicit consent, profile synchronization, your own external_id values, test/live environments, hiding, erasure and account restoration under the documented rules. - **Discovery.** Filtered feeds/search with cursors, ordinary likes, dislikes, incoming likes and matches. Match lifetime is not a commercial service. - **Chat and events.** Messages with optional translation and original-text fallback, read receipts and history; signed webhooks or polling, durable message retries and handling of late events/erasures. - **Safety.** Moderation, photo checks, reports, blocks, retention and platform isolation. Capabilities describe configuration, not external AI health. - **Optional services.** Boost, superlike, incognito and rewind all start at zero price. Live RUB checkout is isolated from TEST credits. Spending-account enrollment is explicit; existing integrations retain legacy behavior until then. - **Initial release limitations.** Production video is disabled; calls are unavailable. Positive RUB payments, automated bonus issuance and commerce actions through MCP are not provided yet. Basic MCP tools remain available; the MCP protocol version stays `2024-11-05`. See the [API reference](/en/docs/reference/) for fields/errors and [optional services](/en/docs/services/) for their terms. ## Reading future versions - `1.0.1`: backward-compatible fix. - `1.1.0`: backward-compatible addition. - `2.0.0`: incompatible contract with a separate migration plan. We give platforms at least 60 days’ notice of breaking changes and maintain the previous version during that period. Site release versions are independent of the API version. A feature must be available in your environment even if already documented. ```yaml openapi: 3.1.0 info: title: Pereval Partner API version: 1.0.0 summary: Connect a dating app to the Pereval network — shared feed, likes, matches and translated chat across apps. description: > Pereval (pereval.dating) connects dating apps and sites into one network. A user stays in **your** app, but sees people from other apps in the network, likes them and chats with them (machine-translated when languages differ). Basic network integration is free for platforms. Optional network services are separate; all four start at zero RUB. Platforms set their own user-facing pricing (see the network rules). API 1.0.0 is the first stable public contract. Existing /v1 HTTP paths and payloads are retained. Older 0.x labels describe prelaunch development, not additional public API versions. Check optional capabilities per environment; version alone does not enable them. ## How an integration works 1. Apply on pereval.dating. Once approved, issue an API key in the dashboard — separately for the test and live environments. 2. Your backend pushes the profile of every user who opted in: `PUT /v1/members/{external_id}` (or `PUT /v1/members` in batches). 3. Your app shows `GET /v1/members/{external_id}/feed` next to (or mixed with) your own users. 4. Likes go to `POST /v1/likes`, "seen and skipped" to `POST /v1/dislikes`. A mutual like creates a match; both platforms get `match.created`. 5. Messages go to `POST /v1/matches/{match_id}/messages`; the other side's platform gets `message.created` translated into its user's language. Report reading with `POST /v1/matches/{match_id}/read`. 6. The network notifies you through signed webhooks (see `webhooks`) — or you pull events with `GET /v1/events`. ## Optional network services (API 1.0.0) Boosts, superlikes, incognito and rewind are optional. Basic integration does not require them. All four live SKUs start at zero RUB; TEST credits remain isolated and are not money. Check `commerce_enabled`, `commerce_live_zero_only` and `commerce_pilot` in capabilities. Live entitlement enforcement requires explicit administrative enrollment and a separate spending key; existing live clients retain legacy superlike/rewind behavior until enrollment. Positive RUB payments are not supported in this release. See https://pereval.dating/en/docs/services/ for onboarding. Enrolled partners use `X-Action-Ref` for superlikes/rewind and `X-Commerce-Key` for order commits, including zero-price orders. Retry the same logical action/order with the same reference. Opt-in boost delivery supports swipe/search independently of the buyer's UI. Map-based platforms can buy boosts, but map receiving is not supported. Stable cursors and X-Feed-Request-Ref identify logical searches; promotion metadata counts API placements, never measured human views. ## Conventions - Base URL: `https://api.pereval.dating` for both environments; the key decides which one. Auth: `Authorization: Bearer `. - `external_id` is **your** user id (any string, 1–128 chars). `network_id` is the network's id of a member of another app (UUID). You never see other platforms' internal ids. - All bodies are JSON (UTF-8). Times are RFC 3339 in UTC. - Errors: HTTP status + `{"error": "", "message": ""}`. Codes are stable; messages are not. `503 internal` means the network is temporarily unavailable (even on auth) — retry with a delay, your key is fine. - Photo verification: tell the network how and when you checked that a person matches their photos (`verification`). The `photo_verified` mark is computed by the network: it counts once the network has approved your verification method, and only for photos that are not newer than the check. - Photo check by the network: no verification of your own? `POST /v1/photo-checks` gives a link to a network page where the person shows a gesture and takes a picture; you get a per-photo result and may then claim `verification: {provider: "pereval", check_id}` — it counts even before your own method is approved. Free. - Rate limit: 600 requests/minute per platform (burst 100) → `429 rate_limited`. - Users must opt in. Never push a user who hasn't explicitly enabled "show me in the network". - Languages are ISO 639-1 codes (`de`, not `German`). The network translates between 38 languages — `GET /v1/languages`. - Inspect `GET /v1/capabilities` before enabling optional functionality. Videos are disabled in production until frame moderation exists; nonempty video input is rejected. - Webhooks are at-least-once: deduplicate by event `id`. Pending events retain causal `seq` order per partner/recipient, including retries; different recipients progress independently. Expired (7 days) or permanently failed events are not replayed forever: reconcile matches/history after an outage. - Suspending a platform immediately hides its members from feed/likes and prohibits new likes/messages in either direction. Queued webhooks pause without deletion; normal 7-day event retention still applies. Resumption does not remove member-level freezes or reopen closed/erased matches. - Personal report materials have an absolute 60-day active-database limit from original submission; photo-check materials and metadata have the same limit from original check creation, including unresolved disputes. Disputes, appeals, updates and decisions do not extend it. Expiry is neither photo approval nor an unban. Operators must review before expiry. Message, erased-chat archive and outbox retention are unchanged. Closed backups are retained for at most 30 further days; purge before the snapshot is mandatory to keep the total within 90 days. Backup guarantees depend on the infrastructure pre-snapshot cleanup/deadline gate. contact: name: Pereval partner support url: https://pereval.dating/en/contact/ license: name: Open protocol — implement freely url: https://pereval.dating/en/docs/ servers: - url: https://api.pereval.dating description: Test and live — the API key selects the environment security: - bearer: [] tags: - name: Members description: Push, update and remove your users' network profiles. - name: Feed description: People from other apps who fit your user. - name: Likes & matches description: Likes and dislikes across apps; a mutual like creates a match that opens chat. - name: Messages description: Text chat inside a match, machine-translated when the recipient doesn't speak the sender's language. - name: Safety description: Blocks and reports. Reports reach the reported member's platform; serious ones suspend the member network-wide. - name: Languages description: Languages the network translates profiles and messages into. - name: Photo check description: > The network's photo check for platforms without their own: the person takes it on a network page (consent, a requested gesture, a camera picture); the network compares the picture with each profile photo. - name: Events description: Pull events yourself — for platforms without a webhook URL and for recovery after an outage. - name: MCP description: > Model Context Protocol JSON-RPC 2.0 for AI agents. Same Bearer key as REST. POST /mcp — methods initialize, tools/list, tools/call, ping. Protocol version 2024-11-05. paths: /v1/members/{external_id}/boost: get: summary: Current own boost and remaining time; voluntary cancellation is not supported security: - External1_PartnerKey: [] parameters: - name: external_id in: path required: true schema: type: string responses: "200": description: Boost state content: application/json: schema: $ref: "#/components/schemas/External1_Boost" "404": description: Own boost not found "503": description: Pilot unavailable operationId: commerceBoost tags: - Commerce /v1/commerce/delivery: put: summary: Declare receiving support independently of purchases security: - External1_PartnerKey: [] requestBody: required: true content: application/json: schema: type: object required: - format - enabled properties: format: enum: - swipe - search - none enabled: type: boolean responses: "200": description: Configured format version 1 "422": description: Unsupported/inconsistent format; map receiving is not implemented "503": description: Pilot unavailable operationId: commerceDelivery tags: - Commerce /v1/commerce/catalog: get: summary: Catalog in the enrolled account currency security: - External1_PartnerKey: [] responses: "200": description: Success content: application/json: schema: type: object properties: currency: type: string enum: - TEST - RUB test_only: type: boolean items: type: array items: $ref: "#/components/schemas/External1_Product" required: - currency - test_only - items "401": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "402": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "403": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "404": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "409": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "422": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "429": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "503": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" operationId: commerceCatalog tags: - Commerce /v1/commerce/balance: get: summary: TEST credits or zero-only RUB balance security: - External1_PartnerKey: [] responses: "200": description: Success content: application/json: schema: type: object properties: currency: type: string enum: - TEST - RUB test_only: type: boolean balance: type: integer format: int64 minimum: 0 required: - currency - test_only - balance "401": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "402": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "403": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "404": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "409": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "422": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "429": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "503": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" operationId: commerceBalance tags: - Commerce /v1/commerce/quotes: post: summary: Create a five-minute price snapshot; no debit security: - External1_PartnerKey: [] responses: "200": description: Success content: application/json: schema: $ref: "#/components/schemas/External1_Quote" "401": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "402": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "403": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "404": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "409": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "422": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "429": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "503": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" requestBody: required: true content: application/json: schema: type: object properties: member: type: string minLength: 1 maxLength: 256 sku: type: string minLength: 1 maxLength: 80 quantity: type: integer minimum: 1 maximum: 1000000 required: - member - sku - quantity operationId: commerceQuote tags: - Commerce /v1/commerce/orders: post: summary: Atomically purchase entitlement; durable replay by external_order_ref security: - External1_PartnerKey: [] External1_SpendKey: [] responses: "200": description: Success content: application/json: schema: $ref: "#/components/schemas/External1_Order" "401": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "402": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "403": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "404": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "409": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "422": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "429": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "503": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" requestBody: required: true content: application/json: schema: type: object properties: quote_id: type: string format: uuid external_order_ref: type: string minLength: 1 maxLength: 128 required: - quote_id - external_order_ref operationId: commerceOrder tags: - Commerce /v1/members/{external_id}/entitlements: get: summary: List own subject entitlements security: - External1_PartnerKey: [] responses: "200": description: Success content: application/json: schema: type: object properties: items: type: array items: $ref: "#/components/schemas/External1_Grant" required: - items "401": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "402": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "403": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "404": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "409": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "422": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "429": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "503": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" parameters: - name: external_id in: path required: true schema: type: string operationId: commerceEntitlements tags: - Commerce /v1/members/{external_id}/incognito: put: summary: Explicitly enable incognito with a valid right or freely disable it security: - External1_PartnerKey: [] responses: "200": description: Success content: application/json: schema: type: object properties: enabled: type: boolean required: - enabled "401": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "402": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "403": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "404": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "409": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "422": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "429": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" "503": description: See commerce-pilot.md for error codes content: application/json: schema: $ref: "#/components/schemas/External1_Error" requestBody: required: true content: application/json: schema: type: object properties: enabled: type: boolean required: - enabled parameters: - name: external_id in: path required: true schema: type: string operationId: commerceIncognito tags: - Commerce /healthz: get: security: [] operationId: health summary: Process liveness responses: "200": description: Process is running /readyz: get: security: [] operationId: ready summary: Database readiness, not a check of external moderation or AI availability responses: "200": description: Database responds within two seconds "503": description: Database unavailable (not_ready) /v1/capabilities: get: operationId: capabilities summary: Runtime protocol and feature configuration responses: "200": description: Configuration, not proof that external dependencies are healthy content: application/json: schema: type: object required: - protocol_version - production - client_message_id - videos - photo_checks - profile_moderation - commerce_enabled - commerce_live_zero_only - commerce_pilot properties: protocol_version: type: string const: 1.0.0 production: type: boolean commerce_enabled: type: boolean description: Full network commerce is enabled; does not imply this partner is enrolled commerce_live_zero_only: type: boolean description: Live checkout accepts zero RUB only; no monetary payment processor commerce_pilot: type: boolean description: TEST-only development pilot; false in production client_message_id: type: boolean const: true videos: type: boolean description: False in the production executable photo_checks: type: boolean description: Facecheck URL is configured profile_moderation: type: boolean description: Profile moderation is configured "401": description: Missing invalid or suspended platform key: null /mcp: post: tags: - MCP operationId: mcp summary: Model Context Protocol (JSON-RPC) description: > Same API key as the REST API. Methods: `initialize`, `tools/list`, `tools/call`, `ping`. Tools: `upsert_member`, `get_feed`, `like`, `dislike`, `send_message`, `list_events`, `list_languages`. A `tools/call` result is JSON in `result.content[].text`. RPC errors use JSON-RPC `error`; tool failures set `isError: true` and keep HTTP 200. Missing key — HTTP 401. requestBody: required: true content: application/json: schema: type: object required: - jsonrpc - method properties: jsonrpc: type: string enum: - "2.0" id: description: number, string or null (null = notification, no response) method: type: string example: tools/list params: type: object responses: "200": description: JSON-RPC result or error "401": description: Missing or invalid API key /v1/members: put: tags: - Members operationId: upsertMembers summary: Add or update members in a batch description: > Up to 100 members per request, each validated on its own — one bad profile doesn't block the rest. Use it for the first upload of users who turned the network on. requestBody: required: true content: application/json: schema: type: object required: - items properties: items: type: array minItems: 1 maxItems: 100 items: allOf: - type: object required: - external_id properties: external_id: type: string minLength: 1 maxLength: 128 description: Your user id - $ref: "#/components/schemas/MemberInput" responses: "200": description: Per-item results in the same order content: application/json: schema: type: object required: - items properties: items: type: array items: type: object required: - external_id properties: external_id: type: string network_id: type: string format: uuid description: Present when stored photo_verified: type: boolean description: Whether the verification mark counts — see the single upsert verification: $ref: "#/components/schemas/VerificationStatus" error: type: object description: Present when this item was rejected properties: code: type: string enum: - invalid - location_change_limited message: type: string "422": $ref: "#/components/responses/Invalid" /v1/members/{external_id}: parameters: - $ref: "#/components/parameters/ExternalID" put: tags: - Members operationId: upsertMember summary: Add or update a member description: > Idempotent upsert by `external_id`. Call it when a user opts in and whenever their profile, photos, preferences or consents change. **Hidden members.** `consent.network: false` hides the member instead of removing them: the same `network_id`, matches, conversations, dislikes and blocks stay; the member is not in anyone's feed and gets no feed (`409 member_hidden`); they can only like back someone who already liked them (that is a match), no super likes. Messages in existing matches flow both ways. Photos are optional while hidden. Send `consent.network: true` again and the member is visible with all their history. Use this when the user turns the network off; use DELETE only to erase their network data (an explicit "delete my data from the network" or account deletion). Coordinates are rounded to 0.01° (~1 km) by the network; send city-level precision anyway. The city may change once a day: a new city (or a move over 50 km) sooner than 24 h after the previous change returns `409 location_change_limited`. The bio's language is detected by the network; the bio is translated on demand into the main languages of members who are reading the feed. **Moderation.** A new profile, or one with changed photos, name or bio, is checked before it appears in feeds — usually within seconds. Explicit photos, insults or obscenity, contact details (phone, link, messenger handle) and photos that cannot be downloaded are not allowed. On rejection you get `member.rejected` with the reason; the profile is hidden and cannot like until you send a corrected one (resending the same profile does not trigger a new check). Photos and text that already passed are not checked again. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MemberInput" example: consent: network: true cross_language: true world: false given_at: 2026-10-24T12:00:00Z text_version: 2026-10-24 display_name: Anna birth_year: 1994 gender: female age_min: 28 age_max: 40 languages: - ru - en city: name: Moscow country: RU lat: 55.75 lon: 37.62 bio: Hiking, jazz and good coffee photos: - https://cdn.example-dating.app/u/123/1.jpg photo_verified: true height_cm: 168 responses: "200": description: Stored content: application/json: schema: type: object required: - network_id properties: network_id: type: string format: uuid description: The member's id in the network. Stable across updates. photo_verified: type: boolean description: "Whether the verification mark counts: other apps see it and `verified_only` keeps the person" verification: $ref: "#/components/schemas/VerificationStatus" "400": $ref: "#/components/responses/BadJSON" "401": $ref: "#/components/responses/Unauthorized" "409": description: "`location_change_limited` — the city can change once a day" content: application/json: schema: $ref: "#/components/schemas/Error" "422": $ref: "#/components/responses/Invalid" "429": $ref: "#/components/responses/RateLimited" delete: tags: - Members operationId: removeMember summary: Erase a member from the network description: > Call when the user deletes their account or asks to delete their data from the network (to just turn the network off, send `consent.network: false` — see the upsert). Their open matches are closed: the other platforms get `match.closed` with `reason` (`account_deleted` or `left_network`) and `peer_deleted: true` — show the chat as "Account deleted" / "Left the network", read-only. the messages stay with the other person (like Telegram); the profile (name, photos, bio, city), likes, dislikes and blocks are erased. If the user comes back, it is a new member. parameters: - name: reason in: query description: "`account_deleted` or `left_network` (default) — goes to the other side in `match.closed`" schema: type: string enum: - left_network - account_deleted default: left_network responses: "204": description: Removed "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /v1/members/{external_id}/deactivate: parameters: - $ref: "#/components/parameters/ExternalID" post: tags: - Members operationId: deactivateMember summary: The user is deleting their account (can restore within 30 days) description: > Deferred account deletion: the member disappears from feeds and "liked you", their matches are frozen — the other platforms get `match.frozen` and should show "Account deleted"; nobody can write (`409 match_frozen`). Nothing is erased. Call `/restore` if the user comes back, or `DELETE /v1/members/{external_id}?reason=account_deleted` when the 30 days are over; if you don't, the network erases the member itself after 30 days. responses: "204": description: Deleting (idempotent) "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/members/{external_id}/reinstate: parameters: - $ref: "#/components/parameters/ExternalID" post: tags: - Members operationId: reinstateMember summary: Bring back a member the network hid after reports description: > Your moderator reviewed the reports (`member.reported`) and found nothing wrong. The open reports are closed, the member is back in feeds and their frozen matches resume (`match.unfrozen` to the other platforms). A member who is deleting their account can't be reinstated (`403`). responses: "204": description: Reinstated (idempotent) "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/members/{external_id}/restore: parameters: - $ref: "#/components/parameters/ExternalID" post: tags: - Members operationId: restoreMember summary: The user restored their account description: Everything is back as it was; the frozen matches resume (`match.unfrozen` to the other platforms). responses: "204": description: Restored (idempotent) "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/members/{external_id}/active: parameters: - $ref: "#/components/parameters/ExternalID" post: tags: - Members operationId: touchMember summary: Mark a member as active now description: Feeds prefer recently active people and hide those inactive for 30+ days. For many users use `POST /v1/activity`. responses: "204": description: Updated "404": $ref: "#/components/responses/NotFound" /v1/activity: post: tags: - Members operationId: touchMembers summary: Mark many members as active description: Up to 1000 external ids — e.g. everyone who opened the app in the last few minutes. Unknown ids are skipped. requestBody: required: true content: application/json: schema: type: object required: - external_ids properties: external_ids: type: array minItems: 1 maxItems: 1000 items: type: string responses: "200": description: How many members were found and updated content: application/json: schema: type: object properties: updated: type: integer "422": $ref: "#/components/responses/Invalid" /v1/members/{external_id}/feed: parameters: - $ref: "#/components/parameters/ExternalID" get: tags: - Feed operationId: getFeed summary: People from other apps for this member description: > Members of **other** platforms only (you show your own users yourself). A candidate appears when: opposite gender; each one's age is inside the other's range; within 50 km or `radius_km` (or the same city and country when coordinates are missing); they share a language, or both consented to cross-language chat; they share an intent; active in the last 30 days; not liked, not disliked in the last 30 days; no block either way. Most recently active first. Filters only narrow what the viewer's profile already allows. `age_min`/`age_max` intersect the viewer's range (an empty intersection is an empty feed and `200`, not an error; `age_min` > `age_max` in the query is `422`). `intent` must be one of the viewer's intents, or `422`. `language` is an ISO 639-1 code and still requires a shared language or both consents to cross-language chat. `gender`, if sent, must be the opposite of the viewer, or `422`. Pages use `cursor` from the previous response's `next_cursor`. The same request without `cursor` returns the same people. A member sees at most 200 distinct new people a day (UTC) — `remaining_today`. Repeating people already shown today does not spend it. At zero, new people stop and people already shown today still come back. A dislike hides a profile for 30 days; it is not how you turn the page. A member hidden after reports or deleting their account gets no feed (`403`). `bio` is translated into the viewer's main language when the viewer doesn't speak its language — show the "translated" mark and let the user see `bio_original`. Translations are made on demand: the first time a profile is shown to someone with a new main language it may come in the original (`bio_translated: false`), and later feeds carry the translation. `scope=world` drops the distance condition: people from any city and country, but only those with `consent.world`. The viewer needs that consent too, otherwise `409 world_consent_required`. Language and intent conditions still apply. The 200 a day limit is shared by both scopes. Show the city and `country` on the card. Viewer filters: `radius_km` widens or narrows the nearby search (ignored with `scope=world`); `verified_only=true` keeps only `photo_verified` profiles (— marks the network counts, see `verification`); `height_min`/`height_max` keep people within that height — people who didn't specify their height still pass, so a height filter never empties the feed by itself. Store your users' filters yourself and send them with every request. `lat`+`lon` (and optional `city`/`country`) search nearby around that point instead of the viewer's profile city — the profile city does not change. `distance_km` is present only when that person turned on `show_distance`. parameters: - name: X-Feed-Request-Ref in: header required: false description: Logical search reference for opt-in boost delivery; preserve across retries/pages, change for a new search. schema: type: string maxLength: 128 - name: limit in: query schema: type: integer minimum: 1 maximum: 50 default: 20 - name: scope in: query schema: type: string enum: - nearby - world default: nearby - name: radius_km in: query description: Search radius for `scope=nearby` schema: type: integer minimum: 1 maximum: 300 default: 50 - name: verified_only in: query description: Only people with `photo_verified` schema: type: boolean default: false - name: height_min in: query description: Minimum height, cm; people without a height pass schema: type: integer minimum: 120 maximum: 230 - name: height_max in: query description: Maximum height, cm, not less than `height_min`; people without a height pass schema: type: integer minimum: 120 maximum: 230 - name: lat in: query description: Search centre latitude for `scope=nearby`; requires `lon`. Does not change the viewer's city. schema: type: number minimum: -90 maximum: 90 - name: lon in: query description: Search centre longitude for `scope=nearby`; requires `lat` schema: type: number minimum: -180 maximum: 180 - name: city in: query description: Place name for the same-city fallback when a profile has no coordinates schema: type: string - name: country in: query description: ISO 3166-1 alpha-2 with `city` schema: type: string - name: age_min in: query description: Narrows the viewer's age range. Empty intersection with the profile returns an empty feed, not 422. schema: type: integer minimum: 18 maximum: 100 - name: age_max in: query description: Narrows the viewer's age range. Must be ≥ age_min when both are set. schema: type: integer minimum: 18 maximum: 100 - name: intent in: query description: One intent from the viewer's profile. Candidates must have it too. schema: type: string enum: - serious - casual - friendship - arrangement - name: language in: query description: ISO 639-1. Candidates must list it. Does not bypass the shared-language rule. schema: type: string minLength: 2 maxLength: 2 - name: gender in: query description: Must be the opposite of the viewer. schema: type: string enum: - male - female - name: cursor in: query description: Opaque next page, from the previous next_cursor. Invalid cursor is 422. schema: type: string responses: "200": description: Candidates content: application/json: schema: type: object required: - items - remaining_today properties: items: type: array items: $ref: "#/components/schemas/Profile" remaining_today: type: integer description: How many new people this member can still be shown today (UTC). Repeats do not decrease it. next_cursor: type: string description: Pass as cursor for the next page. Absent on the last page. "404": $ref: "#/components/responses/NotFound" "409": description: "`world_consent_required` — `scope=world` for a member without `consent.world`; `member_hidden` — the member is hidden (`consent.network: false`)" content: application/json: schema: $ref: "#/components/schemas/Error" "422": $ref: "#/components/responses/Invalid" /v1/likes: post: tags: - Likes & matches operationId: like summary: Like a member of another app description: > The liked member's platform gets `like.received`. If the like is mutual a match is created and both platforms get `match.created`. Liking a member of your own app through the network is not allowed (`403`). - You can only like someone your user could see: a block either way, the other environment (test vs live), or a hidden member who hasn't liked your user → `404`, as if the profile didn't exist. - A repeated like changes nothing and sends no event; upgrading a like to a super like sends `like.received` again. - While your user's profile is in moderation, their likes are stored but the recipient gets `like.received` only after the profile passes; if it is rejected, shown likes are withdrawn (`like.withdrawn`, `rejected`). **Super like**: `super: true` with an optional `message` (up to 200 characters, no contact details — `422`). The recipient sees it right away, before any match: `like.received` has `super: true` and the sender's profile carries `super_like` with the message in the recipient's language; in the feed and in incoming likes such a person comes first. Show it clearly highlighted. When a match happens, the message becomes the first message of the chat (`message.created` to the recipient's platform). At most 5 super likes a day per member — then `429 super_like_limit`; a regular like upgraded to a super like counts too. How many your users get (and whether they pay) is up to you. parameters: - name: X-Action-Ref in: header required: false description: Required for superlikes after commerce enrollment, including zero-price entitlements. Same reference only for the same action payload. schema: type: string maxLength: 128 requestBody: required: true content: application/json: schema: type: object required: - from - to properties: from: type: string description: Your user's external_id to: type: string format: uuid description: network_id from the feed super: type: boolean default: false message: type: string maxLength: 200 description: "Super like only: shown before the match, becomes the first message after it" responses: "200": description: Like stored content: application/json: schema: type: object required: - matched properties: matched: type: boolean match_id: type: string format: uuid description: Present when matched "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: "`member_hidden` — a hidden member can only like back someone who liked them, without a super like" "422": $ref: "#/components/responses/Invalid" "429": description: "`super_like_limit` — more than 5 super likes in 24 h" /v1/likes/{network_id}: delete: tags: - Likes & matches operationId: unlike summary: Take a like (or super like) back before it became a match description: The liked member's platform gets `like.withdrawn`. After a match — `409 matched`; close the match instead. parameters: - name: X-Action-Ref in: header required: false description: Required after commerce enrollment. Replays of the same undo must use the same reference. schema: type: string maxLength: 128 - name: network_id in: path required: true schema: type: string format: uuid - name: from in: query required: true schema: type: string description: Your user's external_id responses: "204": description: Like removed "404": $ref: "#/components/responses/NotFound" "409": description: "`matched` — the like already became a match" /v1/dislikes/{network_id}: delete: tags: - Likes & matches operationId: undislike summary: Bring a skipped profile back description: Removes the dislike — the profile may appear in the feed again. parameters: - name: X-Action-Ref in: header required: false description: Required after commerce enrollment. Replays of the same undo must use the same reference. schema: type: string maxLength: 128 - name: network_id in: path required: true schema: type: string format: uuid - name: from in: query required: true schema: type: string description: Your user's external_id responses: "204": description: Dislike removed "404": $ref: "#/components/responses/NotFound" /v1/dislikes: post: tags: - Likes & matches operationId: dislike summary: Your user saw a profile and didn't like it description: > Send it when your user swiped left or scrolled past a profile without liking. Only your app knows what the user actually saw, so the network never treats a served feed as seen. The profile leaves the user's feed for 30 days. One pair or a batch of up to 100. requestBody: required: true content: application/json: schema: oneOf: - $ref: "#/components/schemas/Pair" - type: object required: - items properties: items: type: array minItems: 1 maxItems: 100 items: $ref: "#/components/schemas/Pair" responses: "204": description: Stored "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /v1/members/{external_id}/likes: parameters: - $ref: "#/components/parameters/ExternalID" get: tags: - Likes & matches operationId: listLikes summary: Who liked this member and waits for an answer description: Without mutual likes, dislikes and blocks. Rebuilds "Liked you" when events were lost. parameters: - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 50 responses: "200": description: Incoming likes, newest first content: application/json: schema: type: object required: - items properties: items: type: array items: type: object required: - from - liked_at properties: from: $ref: "#/components/schemas/Profile" liked_at: type: string format: date-time "404": $ref: "#/components/responses/NotFound" /v1/members/{external_id}/matches: parameters: - $ref: "#/components/parameters/ExternalID" get: tags: - Likes & matches operationId: listMatches summary: Open matches of a member responses: "200": description: Matches, newest first content: application/json: schema: type: object required: - items properties: items: type: array items: type: object required: - match_id - created_at - with properties: match_id: type: string format: uuid created_at: type: string format: date-time with: $ref: "#/components/schemas/Profile" frozen: type: boolean description: Omitted when false freeze_reason: type: string enum: - account_deleting - suspended - partner_suspended description: Present when frozen /v1/matches/{match_id}: parameters: - $ref: "#/components/parameters/MatchID" delete: tags: - Likes & matches operationId: unmatch summary: Close or erase a match (unmatch) description: > Without `erase` the match is closed and both keep the history (`match.closed`, `reason: unmatched`). With `erase=true` the match, its messages and both likes are deleted for both — the other platform gets `match.erased` and should remove the chat; the two won't meet in each other's feeds for `cooldown_days`. If either of them reported the other, a hidden copy of the chat is kept for moderation for 90 days. To hide the history only for your user, use `POST /v1/matches/{match_id}/clear`. parameters: - name: by in: query required: true schema: type: string description: external_id of your user who unmatches - name: erase in: query schema: type: boolean default: false description: Delete the match, messages and likes for both - name: cooldown_days in: query schema: type: integer minimum: 0 maximum: 365 default: 30 description: with `erase`. Days before they can see each other in the feed again; for "never" block instead responses: "204": description: Closed (`match.closed`) or erased (`match.erased`) "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/Invalid" /v1/matches/{match_id}/clear: parameters: - $ref: "#/components/parameters/MatchID" post: tags: - Messages operationId: clearHistory summary: Delete the chat history for one side description: > "Delete chat" for your user only: the history before now is no longer returned to them; the other person keeps everything, the match stays, and new messages go both ways as usual. requestBody: required: true content: application/json: schema: type: object required: - as properties: as: type: string description: external_id of your user responses: "204": description: Cleared "404": $ref: "#/components/responses/NotFound" /v1/matches/{match_id}/messages: parameters: - $ref: "#/components/parameters/MatchID" post: tags: - Messages operationId: sendMessage summary: Send a message description: > The network detects the language from the text; short replies ("ok", "haha", emoji) are attributed to one of the sender's languages, the main one first. If the recipient doesn't speak it, the message is translated in the background into the recipient's main language, and the recipient's platform gets `message.created` once the translation is ready. If translation fails the message is still delivered in the original language (`translated: false`). Cheap scam signals are checked: links, phone numbers and invitations to other messengers early in a chat, talk about money, the same text sent to many people. The message is delivered anyway, with `suspicious` and `flags`, so the recipient's app can show a warning. Members in their first 7 days can start at most 20 conversations a day (`429 first_messages_limit`); replying in existing conversations is unlimited. optional `client_message_id` is sender-scoped (not match-scoped), at most 128 UTF-8 bytes, without surrounding whitespace. Repeating the same ID, match and trimmed text returns the original message, including after match closure; changing match/text returns `409 idempotency_conflict`. Ownership/auth are checked before replay. A suspended platform key is never authorized by a replay. After message retention or explicit match erasure, the original is no longer available: this is not a permanent idempotency reservation. Persist the returned message ID; recover a timed-out response through author history. requestBody: required: true content: application/json: schema: type: object required: - from - text properties: from: type: string description: Sender's external_id text: type: string minLength: 1 maxLength: 4000 client_message_id: type: string minLength: 1 maxLength: 128 description: Optional sender-scoped retry ID; maximum 128 UTF-8 bytes, no surrounding whitespace responses: "200": description: Sent content: application/json: schema: $ref: "#/components/schemas/Message" "404": $ref: "#/components/responses/NotFound" "409": description: match_closed, match_frozen (including partner suspension), or idempotency_conflict content: application/json: schema: $ref: "#/components/schemas/Error" "422": $ref: "#/components/responses/Invalid" "429": description: "`first_messages_limit` — a new member started too many conversations today, or `rate_limited`" content: application/json: schema: $ref: "#/components/schemas/Error" get: tags: - Messages operationId: listMessages summary: Conversation history as seen by a member description: > Newest first by (created_at, id). Messages from the other side are in the viewer's language with the original and scam flags; own messages carry `read_at` and, when supplied, `client_message_id` for retry reconciliation. The `before` message must belong to this match. Closure preserves history; erasure removes it. parameters: - name: as in: query required: true schema: type: string description: external_id of the viewer - name: before in: query schema: type: string format: uuid description: Message id — returns older messages (pagination cursor) - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 50 responses: "200": description: Messages content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: "#/components/schemas/Message" "404": $ref: "#/components/responses/NotFound" /v1/matches/{match_id}/read: parameters: - $ref: "#/components/parameters/MatchID" post: tags: - Messages operationId: markRead summary: Your user read the conversation description: Marks the other side's messages up to `up_to` (inclusive) as read; the other platform gets `message.read`. requestBody: required: true content: application/json: schema: type: object required: - as - up_to properties: as: type: string description: external_id of the reader up_to: type: string format: uuid description: Id of the last message read responses: "204": description: Marked "404": $ref: "#/components/responses/NotFound" /v1/blocks: post: tags: - Safety operationId: block summary: Block a member description: > Both disappear from each other's feeds for good. Without `erase` an open match between them is closed (`match.closed`, `reason: blocked`), the history stays. With `erase: true` the match, messages and likes are deleted for both (`match.erased`, `reason: blocked`); if either reported the other, a hidden copy is kept for moderation for 90 days. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Pair" responses: "204": description: Blocked "404": $ref: "#/components/responses/NotFound" /v1/reports: post: tags: - Safety operationId: report summary: Report a member description: > The reported member's platform gets `member.reported`. A report with `reason: underage` or `commercial`, or reports from 3 different people, suspends the member from the network until review (`member.suspended`): they are out of feeds and their matches are frozen (`match.frozen` with `reason: suspended`, `409 match_frozen` for both). Their platform brings them back with `POST /v1/members/{external_id}/reinstate`. requestBody: required: true content: application/json: schema: type: object required: - from - to - reason properties: from: type: string to: type: string format: uuid reason: type: string enum: - fake - scam - spam - offensive - underage - commercial - other description: "`commercial` — offers sex for money; suspends the member at once" comment: type: string maxLength: 1000 responses: "204": description: Reported "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/Invalid" /v1/languages: get: tags: - Languages operationId: listLanguages summary: Translation languages description: > ISO 639-1 codes the network translates between. Members may list other ISO 639-1 codes in `languages`, but a member whose main language is not in this list reads others in the original. responses: "200": description: Languages content: application/json: schema: type: object required: - translated properties: translated: type: array items: type: string description: ISO 639-1 codes example: translated: - ru - en - uk - be - kk - de - fr - es - it - pt - nl - pl - cs - sk - ro - hu - bg - sr - hr - el - tr - sv - da - fi - he - ar - fa - hi - ja - ko - zh - vi - th - id - ka - hy - az - uz "401": $ref: "#/components/responses/Unauthorized" /v1/photo-checks: post: tags: - Photo check operationId: createPhotoCheck summary: Start a photo check description: > Creates a check session with the person's profile photos (the network downloads them right away) and returns the `url` of the network's check page. Open it for the person — in the browser, a new tab or a webview with camera access; it can't be embedded in an iframe. The link is valid for 30 minutes and allows up to 5 attempts. On the page the person agrees to the comparison, shows the requested gesture and takes a picture with the camera; the network compares it with each photo. The result comes as `photo_check.updated` (after each attempt, a dispute and the moderator's decision) and in `GET /v1/photo-checks/{id}`; the page also posts `{type: "pereval.photo_check", status}` to the window that opened it. The picture is never shown to you or other people and no facial features are stored: it is deleted right after a passed check, kept for 24 hours after a failed one (so the person can dispute the result) and until the moderator's decision if disputed, but never beyond 60 days in the active database from original check creation, including unresolved disputes. Personal metadata, references and source photos are also subject to that absolute limit. Updates, appeals and disputes do not restart it; expiry never approves a photo or removes a sanction. Operators must review before expiry. Public access ends at the earlier public token expiry independently of retained evidence. The consent names your platform and the network, which compares photos on your behalf. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PhotoCheckInput" example: ref: u_123 lang: en return_url: https://your.app/verified photos: - id: ph1 url: https://cdn.your.app/ph1.jpg - id: ph2 url: https://cdn.your.app/ph2.jpg responses: "201": description: Session created — open `url` for the person content: application/json: schema: $ref: "#/components/schemas/PhotoCheck" "401": $ref: "#/components/responses/Unauthorized" "422": $ref: "#/components/responses/Invalid" "429": description: "`photo_check_limit` — more than 10 checks for this person (`ref`) a day; or `rate_limited`" content: application/json: schema: $ref: "#/components/schemas/Error" "503": description: "`photo_check_off` — the comparison service is temporarily unavailable; retry later" content: application/json: schema: $ref: "#/components/schemas/Error" /v1/photo-checks/{id}: get: tags: - Photo check operationId: getPhotoCheck summary: Photo check result parameters: - name: id in: path required: true schema: type: string format: uuid responses: "200": description: The check (without `url`) content: application/json: schema: $ref: "#/components/schemas/PhotoCheck" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /v1/events: get: tags: - Events operationId: pullEvents summary: Pull events description: > Events of your environment in `seq` order after the given cursor; pulled events count as delivered. If your platform has no webhook URL, the network doesn't push — you pull. Events are kept for 7 days. The `data` of each event is the same as in the webhook of that type. parameters: - name: after in: query schema: type: integer default: 0 description: "`next_after` from the previous response (0 — from the oldest kept event)" - name: limit in: query schema: type: integer minimum: 1 maximum: 500 default: 100 responses: "200": description: Events content: application/json: schema: type: object required: - items - next_after properties: items: type: array items: type: object required: - seq - id - type - created_at - data properties: seq: type: integer id: type: string type: type: string created_at: type: string format: date-time data: type: object next_after: type: integer description: Pass as `after` next time webhooks: boost.started: post: operationId: onBoostStarted summary: Boost started description: Durable boost lifecycle event, not a human impression. Reconcile current state with the member boost endpoint. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: boost.started data: type: object required: - member - generation - remaining_ms properties: member: type: string description: Your member external_id generation: type: string format: uuid remaining_ms: type: integer minimum: 0 responses: 2XX: description: Accepted boost.extended: post: operationId: onBoostExtended summary: Boost extended description: Durable boost lifecycle event, not a human impression. Reconcile current state with the member boost endpoint. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: boost.extended data: type: object required: - member - generation - remaining_ms properties: member: type: string description: Your member external_id generation: type: string format: uuid remaining_ms: type: integer minimum: 0 responses: 2XX: description: Accepted boost.paused: post: operationId: onBoostPaused summary: Boost paused description: Durable boost lifecycle event, not a human impression. Reconcile current state with the member boost endpoint. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: boost.paused data: type: object required: - member - generation - remaining_ms properties: member: type: string description: Your member external_id generation: type: string format: uuid remaining_ms: type: integer minimum: 0 responses: 2XX: description: Accepted boost.resumed: post: operationId: onBoostResumed summary: Boost resumed description: Durable boost lifecycle event, not a human impression. Reconcile current state with the member boost endpoint. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: boost.resumed data: type: object required: - member - generation - remaining_ms properties: member: type: string description: Your member external_id generation: type: string format: uuid remaining_ms: type: integer minimum: 0 responses: 2XX: description: Accepted boost.completed: post: operationId: onBoostCompleted summary: Boost completed description: Durable boost lifecycle event, not a human impression. Reconcile current state with the member boost endpoint. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: boost.completed data: type: object required: - member - generation - remaining_ms properties: member: type: string description: Your member external_id generation: type: string format: uuid remaining_ms: type: integer minimum: 0 responses: 2XX: description: Accepted like.received: post: operationId: onLikeReceived summary: Someone from another app liked your user description: Show it as an incoming like. Liking back via `POST /v1/likes` creates a match. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: like.received data: type: object required: - to - from properties: to: type: string description: Your user's external_id from: $ref: "#/components/schemas/Profile" super: type: boolean description: "A super like: `from.super_like` has the message" responses: 2XX: description: Accepted like.withdrawn: post: operationId: onLikeWithdrawn summary: A like your user hasn't answered is gone description: > Remove it from your user's incoming likes. it also comes when the person who liked is no longer available — deleting their account, hidden after reports, their profile rejected by moderation, or removed from the network (`reason`). If they come back (restored, reinstated, profile fixed), your user gets `like.received` again. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: like.withdrawn data: type: object required: - to - from properties: to: type: string description: Your user's external_id from: type: string format: uuid description: network_id of the person who took the like back reason: type: string enum: - unliked - account_deleting - suspended - rejected - account_deleted - left_network description: "`unliked` — they took the like back themselves" responses: 2XX: description: Accepted match.created: post: operationId: onMatchCreated summary: A match was created requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: match.created data: type: object required: - match_id - member - with properties: match_id: type: string format: uuid member: type: string description: Your user's external_id with: $ref: "#/components/schemas/Profile" responses: 2XX: description: Accepted message.created: post: operationId: onMessageCreated summary: A new message for your user description: Show a warning when `suspicious` is true — `flags` say why. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: message.created data: type: object required: - match_id - to - message properties: match_id: type: string format: uuid to: type: string description: Your user's external_id message: type: object required: - id - from - text - translated - lang - created_at - suspicious - flags properties: id: type: string format: uuid description: Use it as `up_to` in markRead from: type: string format: uuid description: Sender's network_id client_message_id: type: string description: Sender-scoped retry ID when supplied; deduplicate incoming messages by message.id text: type: string description: In your user's language when translated text_original: type: string translated: type: boolean lang: type: string description: Language of `text` created_at: type: string format: date-time suspicious: type: boolean flags: $ref: "#/components/schemas/Flags" responses: 2XX: description: Accepted message.read: post: operationId: onMessageRead summary: The other side read your user's messages requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: message.read data: type: object required: - match_id - member - by - up_to - read_at properties: match_id: type: string format: uuid member: type: string description: Your user's external_id (the author) by: type: string format: uuid description: network_id of the reader up_to: type: string format: uuid description: Messages up to this one (inclusive) are read read_at: type: string format: date-time responses: 2XX: description: Accepted match.closed: post: operationId: onMatchClosed summary: A match of your user was closed by the other side description: > Make the chat read-only; the history stays. With `peer_deleted: true` (the other person deleted their account or their network data) also replace their name and photo with "Account deleted" / "Left the network". requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: match.closed data: type: object required: - match_id - member - reason properties: match_id: type: string format: uuid member: type: string reason: type: string enum: - unmatched - blocked - left_network - account_deleted peer_deleted: type: boolean description: "The other person is gone from the network: their profile is erased" responses: 2XX: description: Accepted match.erased: post: operationId: onMatchErased summary: A match of your user was erased by the other side description: The other side deleted the pair (or blocked with erase) — remove the chat and its history on your side. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: match.erased data: type: object required: - match_id - member - reason properties: match_id: type: string format: uuid member: type: string description: Your user's external_id reason: type: string enum: - unmatched - blocked responses: 2XX: description: Accepted match.frozen: post: operationId: onMatchFrozen summary: The other person is deleting their account or was hidden after reports description: > The chat is read-only (`409 match_frozen`). `reason`: `account_deleting` — show "Account deleted"; it may come back with `match.unfrozen` (restored within 30 days) or end with `match.closed` + `peer_deleted`. `suspended` — the network hid them after reports; show "Account unavailable" until `match.unfrozen` (their platform reinstated them) or `match.closed`. `partner_suspended` means either platform is suspended. Restore messaging only after an unfreeze or a fresh bootstrap confirming `frozen: false`; another independent freeze may remain. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: match.frozen data: type: object required: - match_id - member properties: match_id: type: string format: uuid member: type: string description: Your user's external_id reason: type: string enum: - account_deleting - suspended - partner_suspended description: "partner_suspended " responses: 2XX: description: Accepted match.unfrozen: post: operationId: onMatchUnfrozen summary: The other person restored their account or was reinstated after reports description: The chat is back as it was — their name, photo and messaging. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: match.unfrozen data: type: object required: - match_id - member properties: match_id: type: string format: uuid member: type: string description: Your user's external_id responses: 2XX: description: Accepted member.reported: post: operationId: onMemberReported summary: Your user was reported by someone from another app requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: member.reported data: type: object properties: member: type: string reason: type: string comment: type: string responses: 2XX: description: Accepted member.suspended: post: operationId: onMemberSuspended summary: Your user was hidden from the network pending review description: > After a report with `underage` or `commercial`, or reports from 3 different people. Their matches are frozen until you review the reports: block the user in your app or bring them back with `POST /v1/members/{external_id}/reinstate`. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: member.suspended data: type: object properties: member: type: string reason: type: string responses: 2XX: description: Accepted member.rejected: post: operationId: onMemberRejected summary: Your user's profile did not pass the network's moderation description: The profile is hidden from feeds and cannot like until you send a corrected one. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: member.rejected data: type: object required: - member - reason properties: member: type: string reason: type: string enum: - photo_nudity - offensive_text - contact_info - photo_unavailable - video_invalid - video_too_long - video_unavailable photo: type: string format: uri description: The photo or poster that did not pass (photo_* reasons) video: type: string format: uri description: The clip that did not pass (video_* reasons) responses: 2XX: description: Accepted photo_check.updated: post: operationId: onPhotoCheckUpdated summary: A new result of a photo check description: > After each attempt, when the person disputes the result and when the network moderator decides — go by the latest. Passed (`passed` or `resolved` with the photos matched) — you may claim `verification: {provider: "pereval", check_id}`. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: photo_check.updated data: $ref: "#/components/schemas/PhotoCheck" responses: 2XX: description: Accepted ping: post: operationId: onPing summary: Test event from the dashboard description: Sent when you click "Send a test event" in the dashboard. Reply 2xx — this checks the URL and the signature. requestBody: content: application/json: schema: allOf: - $ref: "#/components/schemas/EventEnvelope" - properties: type: const: ping data: type: object properties: message: type: string responses: 2XX: description: Accepted components: securitySchemes: bearer: type: http scheme: bearer description: API key (`dn_…`) issued in the dashboard, one per environment. Keep it on your server — never in a client app. External1_PartnerKey: type: http scheme: bearer External1_SpendKey: type: apiKey in: header name: X-Commerce-Key parameters: ExternalID: name: external_id in: path required: true schema: type: string minLength: 1 maxLength: 128 description: Your user id. MatchID: name: match_id in: path required: true schema: type: string format: uuid schemas: Error: type: object required: - error - message properties: error: type: string enum: - unauthorized - rate_limited - bad_json - bad_id - invalid - not_found - forbidden - match_closed - matched - match_frozen - idempotency_conflict - member_hidden - world_consent_required - location_change_limited - super_like_limit - first_messages_limit - internal message: type: string Pair: type: object required: - from - to properties: from: type: string description: Your user's external_id to: type: string format: uuid description: network_id from the feed erase: type: boolean default: false description: "blocks only: delete the match, messages and likes for both" Flags: type: array description: "Scam signals: `link`, `phone`, `messenger` (early in a chat), `money`, `mass_text` (the same text in several matches, only together with another signal )" items: type: string enum: - link - phone - messenger - money - mass_text MemberInput: type: object required: - consent - display_name - birth_year - gender - age_min - age_max - languages - city - photos properties: consent: type: object required: - network - given_at - text_version properties: network: type: boolean description: The user enabled "show me in the network". `false` hides the member (see the upsert) instead of being rejected cross_language: type: boolean default: false description: The user agreed to chat with people speaking other languages (machine translation) world: type: boolean default: false description: The user wants to meet people from other cities and countries — the `scope=world` feed. Opt-in only, off by default given_at: type: string format: date-time description: When the user actually turned it on text_version: type: string description: Version of the consent text the user saw, e.g. its date display_name: type: string minLength: 1 maxLength: 50 birth_year: type: integer description: Members must be 18+ gender: type: string enum: - male - female description: Matches are always of the opposite gender; there is no "seeking" field (ignored if sent) age_min: type: integer minimum: 18 age_max: type: integer languages: type: array minItems: 1 items: type: string pattern: ^[a-z]{2}$ description: ISO 639-1 description: > First is the main language: profiles and messages are translated into it. Languages listed here are never translated for this member. Translation languages — `GET /v1/languages` (38 in v0.8); other ISO 639-1 codes are accepted, names such as `Russian` are rejected with 422. city: type: object description: Name or coordinates required. Can change once a day. properties: name: type: string country: type: string description: ISO 3166-1 alpha-2 lat: type: number lon: type: number bio: type: string maxLength: 1000 photos: type: array maxItems: 9 items: type: string format: uri pattern: ^https:// description: Public HTTPS URLs; 1–9 while visible, may be empty while hidden videos: type: array maxItems: 3 description: > Short clips shown on the card next to photos, not instead of them. Omit or send `[]` to clear. production rejects all nonempty video input with 422 until frame moderation exists. Inspect the `videos` capability; the following format is for isolated development only. Each clip is an MP4 up to 15 seconds and 20 MB, plus a poster image. The file stays on your host. There is no mute flag: the viewing app starts clips silent and has one switch for every clip. Posters are moderated like photos. A clip that is not an MP4, is longer, or cannot be downloaded rejects the profile (`video_invalid`, `video_too_long`, `video_unavailable`). items: $ref: "#/components/schemas/Video" photo_verified: type: boolean default: false description: > Before 0.15 — "you verified the person matches the photos". Still accepted: without `verification` it is a claim with an unspecified method made when the network receives it. Send `verification` instead. verification: type: object description: > How and when you checked that the person matches their photos. The network turns it into the `photo_verified` mark that other apps see, if (1) the network has approved your verification method — describe it when you connect, the network reviews it once — and (2) none of the current photos is newer than `verified_at`. Photos the person joins the network with are covered by an earlier check too; a photo added later switches the mark off until you send a newer `verified_at`. The same picture under a new URL is recognised by its content. The network re-checks a sample of verified people and those with fake signals (reports, the same photo on another person, generated images); if your marks fail, the network stops counting them for all your users. Omit (and `photo_verified: false`) — no claim. — or a claim by the network's photo check: `{"provider": "pereval", "check_id": "…"}`. The network verifies that the check is yours, has passed and covers all current photos (each with a `match` or `no_face` verdict, at least one `match`, recognised by URL or content); then the mark counts even if your own method isn't approved, and `verified_at` is the time of the check. `method` and `verified_at` are then ignored. anyOf: - required: - method - verified_at - required: - check_id properties: method: type: string enum: - selfie - video - liveness - document - in_person - other description: selfie — a photo selfie (e.g. with a gesture); video — a video selfie; liveness — an automated liveness check; document — an ID with a face match; in_person — met in person provider: type: string maxLength: 60 description: "Who did the check: a vendor (e.g. Sumsub), `own`, or `pereval` — the network's photo check (with `check_id`)" verified_at: type: string format: date-time description: When the check passed. Not in the future. check_id: type: string format: uuid description: > id of a passed photo-check session of yours, less than 60 days from original creation. An expired/deleted reference returns 422 invalid; do not retry it or automatically create a replacement check. Omit a stale reference on subsequent upserts. Already accepted verification of unchanged photos is independent of case material retention; expiry does not approve any new photos. intents: type: array items: type: string enum: - serious - casual - friendship - arrangement default: - serious - casual - friendship description: > What the member is looking for. Two members are matched only if they share an intent. `arrangement` — a relationship with a well-off partner — is opt-in only, requires your app to have the category enabled, and requires `arrangement_role`. Members whose only shared intent is `arrangement` are matched only with the opposite role. Offering money for sex is forbidden in every category. arrangement_role: type: string enum: - provider - seeker description: "With the arrangement intent: `provider` — ready to support a partner; `seeker` — looking for a well-off partner" last_active_at: type: string format: date-time height_cm: type: integer minimum: 120 maximum: 230 description: Height in cm optional — omit if unknown: null show_distance: type: boolean default: false description: Others may see `distance_km` to this person. Off by default. Does not change the city. Video: type: object description: A short clip on a profile card. The bytes stay on the partner's host. required: - url - poster properties: url: type: string format: uri pattern: ^https:// description: Public MP4, at most 15 seconds and 20 MB poster: type: string format: uri pattern: ^https:// description: Still frame. Shown before playback and by apps that do not play video. Moderated like a photo VerificationStatus: type: string enum: - accepted - none - no_photos - partner_not_approved - photos_changed description: > Why the verification mark counts or not: `accepted`; `none` — no claim; `no_photos`; `partner_not_approved` — the network hasn't approved your verification method yet (never for claims by the network's photo check, ); `photos_changed` — a photo is newer than `verified_at`: check the person again and send a newer `verified_at`. Profile: type: object description: A member of another app as seen by your user. required: - network_id - partner - display_name - age - gender - city - languages - bio - bio_translated - photos - photo_verified - intents properties: network_id: type: string format: uuid partner: type: object properties: id: type: string format: uuid name: type: string description: Name of the platform where the person registered. display_name: type: string age: type: integer gender: type: string enum: - male - female city: type: string country: type: string description: ISO 3166-1 alpha-2 distance_km: type: integer description: only if this person turned on `show_distance`. Kilometres from the search centre (viewer's city, or `lat`/`lon`) languages: type: array items: type: string bio: type: string description: In the viewer's main language when the viewer doesn't speak the original bio_original: type: string bio_translated: type: boolean description: Show a "translated" mark when true photos: type: array items: type: string format: uri videos: type: array description: but disabled in the production executable pending frame moderation. Absent when videos capability is false. items: $ref: "#/components/schemas/Video" photo_verified: type: boolean description: The person's app verified they match the photos, and the network counts it verified_by: type: string enum: - partner description: "Who verified the photos: `partner` — the person's app, with a method approved by the network" intents: type: array items: type: string arrangement_role: type: string description: Shown only to viewers who have the arrangement intent themselves height_cm: type: integer description: Absent when the person didn't specify it super_like: type: object description: This person sent the viewer a super like (feed, incoming likes, like.received) properties: message: type: string description: In the viewer's main language when they don't speak the original message_original: type: string translated: type: boolean lang: type: string flags: $ref: "#/components/schemas/Flags" Message: type: object required: - id - from - text - translated - lang - created_at properties: id: type: string format: uuid from: type: string description: "\"me\" for the viewer's own messages, otherwise the sender's network_id" client_message_id: type: string description: Author history/POST echo when supplied; also included in message.created for reconciliation text: type: string text_original: type: string translated: type: boolean lang: type: string created_at: type: string format: date-time suspicious: type: boolean description: Messages from the other side only flags: $ref: "#/components/schemas/Flags" read_at: type: string format: date-time description: "Own messages only: when the other side read it" PhotoCheckInput: type: object required: - ref - photos properties: ref: type: string minLength: 1 maxLength: 200 description: How you recognise the person in the result, usually their `external_id` photos: type: array minItems: 1 maxItems: 9 items: type: object required: - url properties: id: type: string maxLength: 100 description: Your photo id, unique in the session; default — the position (1, 2, …) url: type: string format: uri pattern: ^https:// description: Public HTTPS URL of a profile photo lang: type: string enum: - ru - en default: en description: Language of the check page return_url: type: string format: uri pattern: ^https:// maxLength: 500 description: "Optional: the page's \"Back\" button" PhotoCheck: type: object required: - id - ref - status - attempts - expires_at - photos - created_at properties: id: type: string format: uuid ref: type: string url: type: string format: uri description: "Only in the `POST` reply: the check page to open for the person" status: type: string enum: - created - passed - partial - failed - disputed - resolved - expired description: > `created` — not taken yet; `passed` — it's the person on every photo with a face; `partial` — some photos don't look like the picture; `failed` — see `reason`; `disputed` — the person disputed, a network moderator decides (usually within 24 hours); `resolved` — the moderator decided (`note` explains); `expired` — the link expired untaken. reason: type: string enum: - no_match - no_face - many_faces - no_gesture - virtual_camera description: Why it failed attempts: type: integer description: Pictures taken (up to 5 per session) expires_at: type: string format: date-time description: Until when the link allows attempts checked_at: type: string format: date-time description: The last picture dispute_until: type: string format: date-time description: "Failed or partial: the person can dispute until then" disputed_at: type: string format: date-time decided_at: type: string format: date-time description: The moderator's decision note: type: string description: The moderator's explanation for the person photos: type: array items: type: object required: - id - url properties: id: type: string url: type: string format: uri verdict: type: string enum: - match - no_match - unsure - no_face description: "`match` — the same person; `no_match` — doesn't look like them; `unsure` — couldn't compare reliably; `no_face` — no face, not counted. Absent — not compared yet" created_at: type: string format: date-time EventEnvelope: type: object description: > Every webhook is a POST with this envelope. Headers: `X-Network-Signature: sha256=` — HMAC-SHA256 of the raw body with your signing secret (verify it, constant-time compare); `X-Network-Event: `. Reply 2xx quickly. Non-2xx or timeout (10 s) → retries after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h, then daily for up to 3 days. Deliveries may repeat — dedupe by `id`. Order holds unless a delivery is retried. Without a webhook URL, pull the same events with `GET /v1/events`. required: - id - type - created_at - data properties: id: type: string description: evt_…, unique type: type: string created_at: type: string format: date-time data: type: object External1_Error: type: object properties: error: type: string message: type: string required: - error - message External1_Product: type: object properties: sku: type: string capability: enum: - superlike - rewind - incognito - boost unit: enum: - action - minute - day unit_price: type: integer format: int64 minimum: 0 min_quantity: type: integer minimum: 1 max_quantity: type: integer minimum: 1 fulfillment_ready: type: boolean version: type: integer minimum: 1 currency: type: string enum: - TEST - RUB required: - sku - capability - unit - unit_price - min_quantity - max_quantity - fulfillment_ready - version - currency External1_Quote: type: object properties: id: type: string format: uuid member_id: type: string format: uuid sku: type: string capability: type: string unit: type: string quantity: type: integer minimum: 1 unit_price: type: integer format: int64 minimum: 0 total: type: integer format: int64 minimum: 0 catalog_version: type: integer expires_at: type: string format: date-time currency: type: string enum: - TEST - RUB required: - id - member_id - sku - capability - unit - quantity - unit_price - total - catalog_version - expires_at - currency External1_Order: type: object properties: id: type: string format: uuid external_order_ref: type: string quote_id: type: string format: uuid total: type: integer format: int64 minimum: 0 created_at: type: string format: date-time currency: type: string enum: - TEST - RUB grant_id: type: string format: uuid valid_from: type: string format: date-time valid_until: type: - string - "null" format: date-time required: - id - external_order_ref - quote_id - total - created_at - currency - grant_id - valid_from - valid_until External1_Grant: type: object properties: id: type: string format: uuid capability: type: string quantity: type: integer minimum: 1 used: type: integer format: int64 minimum: 0 valid_from: type: string format: date-time valid_until: type: - string - "null" format: date-time revoked_at: type: - string - "null" format: date-time required: - id - capability - quantity - used - valid_from - valid_until - revoked_at External1_Boost: type: object required: - generation - state - started_at - remaining_ms - api_placements properties: generation: type: string format: uuid state: enum: - active - paused - completed started_at: type: string format: date-time remaining_ms: type: integer minimum: 0 api_placements: type: integer minimum: 0 responses: BadJSON: description: Body is not valid JSON content: application/json: schema: $ref: "#/components/schemas/Error" Unauthorized: description: Missing, invalid or suspended API key content: application/json: schema: $ref: "#/components/schemas/Error" Invalid: description: Validation failed; `message` says which field content: application/json: schema: $ref: "#/components/schemas/Error" NotFound: description: Member, match or message not found (or not yours) content: application/json: schema: $ref: "#/components/schemas/Error" Forbidden: description: Not allowed (e.g. liking or disliking a member of your own app, or a suspended member) content: application/json: schema: $ref: "#/components/schemas/Error" MatchClosed: description: The match is closed content: application/json: schema: $ref: "#/components/schemas/Error" RateLimited: description: Over 600 requests per minute content: application/json: schema: $ref: "#/components/schemas/Error" ```