Referência completa · 28/08/2026
Welleva API
Backend do Welleva — um sistema operacional pessoal de saúde para iOS. O app é local-first: cerca de 90% dos recursos rodam inteiramente no dispositivo. Esta API existe só para o que realmente precisa de servidor.
- Base URL
- https://welleva.app
- Endpoints
- 128
- Versão
- v1
- Auth
- Bearer JWT
- Spec
- openapi.json
Começando
Do primeiro launch ao primeiro registro de refeição.
1 · Configuração remota antes de tudo
No cold start, busque GET /v1/config — funciona sem sessão.
Ele traz feature flags, kill-switches, pesos do score e os limites do plano. Verifique
a assinatura ES256 com a chave pública embarcada no app antes de aplicar qualquer valor:
sem isso, uma rede hostil poderia reescrever os pesos do score ou desligar recursos.
2 · Sign in with Apple
POST /v1/auth/apple troca o identity token da Apple por uma sessão Welleva.
Envie sempre um nonce — é o que impede que um token capturado de outro app
seja reproduzido aqui.
A Apple manda o e-mail do usuário apenas na primeira autorização. Persista naquele momento; não virá de novo.
POST /v1/auth/apple
Content-Type: application/json
{
"identity_token": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ…",
"nonce": "a1b2c3d4e5f6",
"full_name": "Ana Silva",
"device": {
"device_uuid": "B8F3E1A0-…",
"platform": "ios",
"model": "iPhone16,2",
"os_version": "18.2",
"app_version": "1.0.0"
}
}
→ 201 Created
{
"data": {
"user": { "id": "usr_m8k2p4nq7xw3z9a", "username": null, … },
"entitlement": { "plan": "free", "active": false, … },
"is_new_user": true,
"needs_username": true,
"tokens": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9…",
"refresh_token": "4f9c…",
"token_type": "Bearer",
"expires_in": 900
}
}
}
3 · Rotação de sessão
O access token vale 15 minutos. O refresh token é opaco, de uso único e rotacionado a cada uso — guarde o novo imediatamente e descarte o antigo.
Trate 401 token_reuse_detected como “entrar de novo”
Apresentar um refresh token já rotacionado revoga a família inteira de sessões. Ou um atacante está reproduzindo um token roubado, ou o cliente está retentando — e como não há como distinguir os dois, a resposta segura é encerrar a sessão. Não tente recuperar: mande o usuário para o login.
4 · Onboarding gera o plano
POST /v1/me/onboarding devolve calorias, macros, água, passos e sono —
cada meta com uma justificativa em linguagem natural, pronta para exibir. Se você tiver
um TDEE medido de 30 dias do HealthKit, mande em measured_tdee: um número
medido sempre vence uma estimativa por tabela.
5 · Sincronize agregados diários
POST /v1/sync/aggregates aceita até 120 dias por requisição. Depois disso,
desafios, ligas e Wrapped passam a funcionar — todos derivam desses agregados.
Convenções
Valem para toda a superfície da API.
Formato de resposta
Todo sucesso vem envelopado em data, com meta opcional para
paginação, contagens e estado de quota. Todo erro vem em error.
// Sucesso
{ "data": { … }, "meta": { "count": 12, "next_cursor": "8842" } }
// Erro
{ "error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"details": { "fields": { "height_cm": ["The field height_cm must be between 120, 230."] } },
"request_id": "req_0mtcm8o3zdaq3scypyb"
} }
Faça branch em error.code, nunca em error.message
O código é estável e faz parte do contrato. A mensagem é localizada e pode mudar a qualquer release.
Cabeçalhos
| Cabeçalho | Direção | Para quê |
|---|---|---|
Authorization: Bearer … | envio | Access token. Obrigatório em rotas autenticadas. |
X-Welleva-Locale | envio | pt-BR, en ou es. Sobrepõe o locale da conta. |
X-Welleva-App-Version | envio | Build do cliente. Alimenta segmentação de config e analytics. |
X-Idempotency-Key | envio | Torna a repetição segura. Veja abaixo. |
X-Attest-Key-IdX-Attest-AssertionX-Attest-Client-Data | envio | Asserção App Attest nas rotas de IA. |
X-Request-Id | ambos | Correlação. Ecoado na resposta e presente em todo log da requisição. |
X-RateLimit-Remaining | resposta | Requisições restantes no bucket. |
Retry-After | resposta | Segundos até poder tentar de novo (429 e 503). |
Idempotência
Redes móveis derrubam respostas, e o app vai retentar. Mande
X-Idempotency-Key em qualquer coisa que custe dinheiro ou crie um recurso:
a repetição devolve a resposta armazenada em vez de refazer o trabalho.
Sem isso, uma resposta perdida em POST /v1/ai/food/photo significa cobrar
a análise duas vezes. Reutilizar a mesma chave com um corpo diferente é bug de
cliente e retorna 409 idempotency_key_reused.
Paginação
Coleções longas usam cursor. Passe meta.next_cursor da página anterior em
?cursor=. Quando next_cursor vem nulo, acabou.
Limites de requisição
Buckets por endpoint, chaveados pelo usuário quando autenticado e pelo IP quando não. O limite de cada rota aparece como badge na referência.
Endpoints que custam dinheiro falham fechados se o limitador estiver indisponível — o inverso deixaria a conta de IA sem medição.
Erros
Códigos HTTP e o que fazer com cada um.
| Status | Significado | O que o app deve fazer |
|---|---|---|
| 400 | Requisição malformada (JSON inválido) | Bug de cliente. Logar e reportar. |
| 401 | Token ausente, expirado, revogado ou reusado | token_expired → renovar. Qualquer outro → voltar ao login. |
| 402 | Limite do plano free ou recurso Welleva+ | Abrir o paywall que details.paywall indica. |
| 403 | Autenticado, mas sem permissão | Consentimento, opt-in ou atestação faltando. details.next diz o caminho. |
| 404 | Recurso inexistente | Também retornado quando o recurso existe mas o chamador não pode saber disso. |
| 405 | Método HTTP errado | O cabeçalho Allow lista os válidos. |
| 409 | Conflito com o estado atual | Duplicado, já ingressou, chave de idempotência reusada. |
| 422 | Falha de validação | details.fields mapeia campo → mensagens. Exibir inline. |
| 429 | Limite de requisições | Respeitar Retry-After. Não retentar em loop. |
| 500 | Erro inesperado | Citar request_id no suporte. |
| 503 | Dependência fora, ou guardrail de custo acionado | Respeitar Retry-After. Oferecer registro manual. |
Códigos de erro por domínio
| Código | Status | Quando acontece |
|---|---|---|
token_missing | 401 | Sem cabeçalho Authorization. |
token_expired | 401 | Access token venceu. Renove. |
token_reuse_detected | 401 | Refresh token já rotacionado. Família revogada — refaça login. |
session_revoked | 401 | Logout, exclusão de conta ou suspensão. |
account_suspended | 403 | Conta suspensa por moderação. |
premium_required | 402 | Recurso ou limite Welleva+. Veja details.paywall. |
consent_required | 403 | Falta consentimento. details.consent_type diz qual. |
onboarding_required | 403 | Recurso depende do plano gerado no onboarding. |
attestation_required | 403 | Rota de IA sem asserção App Attest. |
attestation_replay | 403 | Asserção já usada. Gere uma nova por requisição. |
ai_daily_budget_reached | 503 | Teto diário de gasto do usuário. Reseta à meia-noite. |
ai_global_budget_reached | 503 | Teto global acionado. Ofereça registro manual. |
ai_feature_disabled | 503 | Kill-switch ativo via configuração remota. |
ai_unparseable_response | 503 | Modelo devolveu algo inesperado. Retentável. |
macros_inconsistent | 422 | Calorias não batem com os macros informados. |
nearby_age_restricted | 403 | Nearby exige 17 anos ou mais. |
wrapped_insufficient_data | 404 | Menos de 10 dias (mês) ou 60 (ano) de dados. |
idempotency_key_reused | 409 | Mesma chave, corpo diferente. |
username_taken | 409 | Username indisponível. |
O limite de privacidade
Leia antes de propor qualquer campo novo.
Amostras brutas do HealthKit nunca saem do dispositivo. Esta API aceita apenas agregados diários, e a allow-list de colunas é o ponto de aplicação: qualquer campo fora dela é ignorado silenciosamente, então um cliente futuro que compartilhe demais não consegue ampliar o que guardamos.
| Nunca armazenado | Por quê |
|---|---|
| Séries de frequência cardíaca, sessões de sono, rotas de treino | Nada no servidor precisa disso. |
| Coordenadas GPS | Nearby guarda um geohash de ~5 km e descarta lat/long na hora. |
| Fotos de progresso | Ficam no dispositivo. |
| Fotos de prato além de 24 h | Processadas, EXIF removido, apagadas por cron. |
O sub da Apple em claro | Guardado só como HMAC com chave — um dump não reverte para o identificador. |
O que é armazenado, e por quê
Um registro por usuário por dia, com os totais que o app escolheu sincronizar. Existe para servir desafios, ligas, percentis do Wrapped e continuidade entre dispositivos — nada além disso precisa de servidor.
Percentis populacionais são calculados a partir de pontos de corte pré-computados, nunca da distribuição bruta. E são omitidos quando a amostra tem menos de 100 pessoas: um ranking tirado de um punhado de usuários é ruído, e pode des-anonimizar em coorte pequena.
Consentimento granular
Cada finalidade de tratamento é consentida separadamente e pode ser revogada a qualquer
momento via POST /v1/me/consents. Duas são bloqueantes:
ai_photo_processing para análise de foto e nearby_location
para o Nearby.
Exclusão de conta
Em duas fases. Imediatamente: a conta é marcada como pendente e todas as sessões são revogadas — a conta para de funcionar no momento do pedido, não quando a exclusão executa. Após 14 dias: dados pessoais são apagados e o registro da conta é anonimizado, o que mantém estatísticas agregadas e registros fiscais válidos sem preservar nenhum identificador.
Planos e limites
O servidor é a autoridade. O StoreKit decide o que o app mostra; esta tabela decide o que a API faz.
| Recurso | Free | Welleva+ |
|---|---|---|
| Análise de foto por IA | 3 / semana | 30 / dia (fair use) |
| Análise por texto | 5 / semana | 60 / dia |
| Busca no catálogo e barcode | ilimitado | ilimitado |
| Breakdown dos scores | — | completo |
| Body Age e Digital Twin | — | sim |
| Cause & Effect (padrões) | — | sim |
| Metas adaptativas | — | sim |
| Desafios ativos | 1 | 20 |
| Criar desafios | — | 10 |
| Nearby | — | sim |
| Wrapped | resumido, com marca d'água | completo e personalizável |
| Exportar dados (CSV/PDF) | — | 10 / mês |
| Fotos de progresso | 1 / mês | 500 / mês |
Por que “ilimitado” tem teto
O fair use de 30 análises/dia é o que sustenta a margem de contribuição de ≥40% no usuário intenso. O caminho quando a quota acaba não é só recusar: créditos avulsos comprados são consumidos automaticamente antes de retornar 402.
Referência de endpoints
Os 128 endpoints, gerados a partir da spec — que por sua vez é validada contra a tabela de rotas em execução. Clique para expandir.
Saúde do serviço
2 endpointsLiveness and readiness probes.
GET /v1/health Liveness probe público
Confirms the API process is serving. Does not touch the database — use /health/ready for that.
Respostas
- 200 Service is up
GET /v1/health/ready Readiness probe público
Checks MariaDB, Redis and the analytics store. Returns 503 when any dependency is unreachable — a load balancer should drain the instance rather than restart it.
Respostas
- 200 All dependencies reachable
- 503 One or more dependencies are down
Autenticação
8 endpointsSign in with Apple, session rotation, device sessions.
POST /v1/auth/apple Sign in with Apple público10/300s
Exchanges an Apple identity token for a Welleva session. Creates the account on first use.
The identity token is verified against Apple's live JWKS: RS256 signature, iss, aud (your bundle id), expiry, and — when you send one — the nonce. Sending a nonce is strongly recommended: it is what stops a token captured from another app being replayed here.
Apple returns the user's email only on the very first authorisation. Persist it then; it will not be sent again.
Returns 201 when the account was created, 200 on a subsequent sign-in.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
identity_tokenobrigatório | string | The JWS from ASAuthorizationAppleIDCredential.identityToken. |
nonce | string? | The raw nonce whose SHA-256 you passed to Apple. |
full_name | string? | Also first-authorisation only. |
device | object |
{
"identity_token": "eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ…",
"nonce": "a1b2c3d4e5f6",
"full_name": "Ana Silva",
"device": {
"device_uuid": "B8F3…",
"platform": "ios",
"model": "iPhone16,2",
"os_version": "18.2",
"app_version": "1.0.0"
}
} Respostas
- 200 Signed in
- 201 Account created
- 401 The Apple token failed verification
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
- 503 Dependência indisponível ou guardrail de custo acionado.
POST /v1/auth/logout Sign out this device auth
Revokes the current session. Takes effect immediately, not when the access token expires.
Respostas
- 204 Signed out
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/auth/logout-all Sign out everywhere else auth
Revokes every other session, keeping the current one. Use after a suspected compromise.
Respostas
- 200 Sessions revoked
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/auth/refresh Rotate the session público30/300s
Exchanges a refresh token for a new pair. The refresh token is single-use — store the new one immediately and discard the old.
Presenting a token that was already rotated revokes the entire session family and returns 401 token_reuse_detected. That is deliberate: either an attacker is replaying a stolen token or the client is retrying, and since the two are indistinguishable, the safe response is to end the session. Handle this code by sending the user back to sign-in.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
refresh_tokenobrigatório | string |
Respostas
- 200 New token pair
- 401 Unknown, expired, revoked or reused token
- 429 Limite de requisições excedido.
GET /v1/auth/sessions List active sessions auth
Powers a "where you are signed in" screen.
Respostas
- 200 Active sessions
- 401 Token ausente, inválido, expirado ou revogado.
DELETE /v1/auth/sessions/{id} Revoke one session auth
Always returns 204, whether or not the session existed — a caller must not be able to probe for other users' session ids.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | integer |
Respostas
- 204 Revoked (or never existed)
- 401 Token ausente, inválido, expirado ou revogado.
PUT /v1/auth/username Claim a username auth2/604800s
3–30 characters: lowercase letters, digits, dot or underscore. Reserved names are refused.
Rate limited to 2 changes per week — usernames are how friends find each other, so churn is disruptive.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
usernameobrigatório | string |
Respostas
- 200 Username claimed
- 409 Taken, reserved or invalid
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/auth/username-available Check username availability auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
usernameobrigatório | query | string |
Respostas
- 200 Availability
- 401 Token ausente, inválido, expirado ou revogado.
Atestação
2 endpointsApp Attest enrolment for the cost-bearing AI routes.
POST /v1/attest/challenge Request an App Attest challenge auth20/3600s
Step 1 of App Attest enrolment. Returns a single-use, 5-minute challenge to pass to DCAppAttestService.attestKey(_:clientDataHash:).
When attestation is not enabled on the server, this returns {"enabled": false} and the app should skip enrolment entirely rather than treating it as an error.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
device_uuidobrigatório | string |
Respostas
- 200 Challenge issued
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
POST /v1/attest/key Register an attested key auth20/3600s
Step 2. Submits the attestation object from the Secure Enclave.
The server verifies the full Apple certificate chain, that the embedded nonce matches the challenge it issued, that the key id is the SHA-256 of the certificate's public key, and that the attestation is bound to this app's Team ID and bundle id. Any mismatch is refused.
Once registered, every /v1/ai/* call must carry an assertion signed by this key.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
device_uuidobrigatório | string | |
key_idobrigatório | string | Base64 key identifier from generateKey. |
attestation_objectobrigatório | string | Base64 CBOR attestation object. |
challengeobrigatório | string | The challenge from step 1. |
Respostas
- 201 Key registered
- 403 Attestation failed verification
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Perfil e metas
12 endpointsAccount, onboarding, goals, privacy settings and consent.
GET /v1/me Get your profile, goals, privacy settings and entitlement auth
The single call to make after sign-in: everything the app needs to render its first screen.
Respostas
- 200 Your account
- 401 Token ausente, inválido, expirado ou revogado.
PATCH /v1/me Update account fields auth
Partial update. Only the fields you send are changed.
Corpo
| Campo | Tipo | Descrição |
|---|---|---|
display_name | string? | (max 80) |
locale | pt-BR · en · es | |
timezone | string | IANA identifier. Drives quiet hours and daily rollovers. |
unit_system | metric · imperial | |
birth_date | string | |
sex | female · male · other · undisclosed | |
height_cm | number | (min 120, max 230) |
country | string | (min 2, max 2) |
Respostas
- 200 Updated
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/me/consents Current consents auth
LGPD/GDPR: which processing purposes the user has agreed to, and at which policy version.
Respostas
- 200 Consents
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/me/consents Grant or revoke a consent auth
Each processing purpose is consented to separately and can be revoked at any time.
ai_photo_processing is required before POST /v1/ai/food/photo will run, and nearby_location before Nearby.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
consent_typeobrigatório | terms · privacy · health_processing · ai_photo_processing · social_sharing · nearby_location · marketing_push · analytics | |
grantedobrigatório | boolean | |
versionobrigatório | string |
Respostas
- 201 Recorded
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/me/goals Current active goals auth
Respostas
- 200 Goals keyed by metric
- 401 Token ausente, inválido, expirado ou revogado.
PUT /v1/me/goals Override goals manually auth
Sets custom targets. Values outside physiological bounds are clamped rather than rejected.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
goalsobrigatório | array<object> |
{
"goals": [
{
"metric": "steps",
"target": 12000
},
{
"metric": "water_ml",
"target": 3000
}
]
} Respostas
- 200 Goals applied
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/me/goals/{metric}/history Goal history for one metric auth
Every past target with its effective window — what makes a historic score explainable.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
metricobrigatório | path | string |
Respostas
- 200 History
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/me/onboarding Complete onboarding and generate the plan auth
Submits the onboarding quiz and returns a complete daily plan: calories, macros, water, steps, sleep, active minutes and mindful minutes — each with a plain-language rationale you can show the user.
Calories use Mifflin-St Jeor for BMR × an activity factor, then adjust for the goal. If you can supply a measured 30-day TDEE from HealthKit in measured_tdee, it overrides the activity-factor estimate — a measured number always beats a table lookup.
Safety rails are applied server-side and cannot be overridden: the weight-change pace is capped at 1.0 kg/week, and the calorie target never drops below 1,200 kcal (female) / 1,500 kcal (male), whatever was requested.
Goals are versioned by effective date rather than overwritten, so a past score stays explainable.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
goal_typeobrigatório | lose_weight · gain_muscle · sleep_better · more_energy · general_health · maintain | |
sexobrigatório | female · male · other · undisclosed | |
birth_dateobrigatório | string | |
height_cmobrigatório | number | (min 120, max 230) |
current_weight_kgobrigatório | number | (min 35, max 300) |
target_weight_kg | number? | |
weight_pace_kg_week | number | (min 0.1, max 1) |
activity_levelobrigatório | sedentary · light · moderate · active · very_active | |
dietary_pattern | string? | |
dietary_restrictions | array<string> | |
measured_tdee | number? | Measured daily energy expenditure from HealthKit. Overrides the estimate. |
baseline_sleep_minutes | integer? | |
baseline_water_ml | integer? | |
caffeine_cups_per_day | integer? |
{
"goal_type": "lose_weight",
"sex": "female",
"birth_date": "1990-05-14",
"height_cm": 168,
"current_weight_kg": 74.5,
"target_weight_kg": 66,
"weight_pace_kg_week": 0.5,
"activity_level": "moderate",
"dietary_restrictions": [
"lactose"
],
"caffeine_cups_per_day": 3
} Respostas
- 201 Plan generated
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"plan": {
"calories": {
"value": 1810,
"unit": "kcal",
"rationale": "TDEE ≈ 2360 kcal (estimated) − 550 kcal for 0.5 kg/week"
},
"protein_g": {
"value": 134,
"unit": "g",
"rationale": "1.8 g per kg of body weight"
},
"water_ml": {
"value": 2900,
"unit": "ml",
"rationale": "35 ml per kg, adjusted for your activity level"
}
}
}
}GET /v1/me/privacy Privacy settings auth
Respostas
- 200 Settings
- 401 Token ausente, inválido, expirado ou revogado.
PATCH /v1/me/privacy Update privacy settings auth
Controls what friends can see. Everything defaults to off or friends-only — sharing is an explicit act.
leaderboard_visibility: anonymous keeps the user competing while hiding their identity; hidden removes them from boards entirely.
Corpo
| Campo | Tipo | Descrição |
|---|---|---|
share_scores | boolean | |
share_streaks | boolean | |
share_activity | boolean | |
share_nutrition | boolean | |
share_weight | boolean | |
discoverable_by_username | boolean | |
nearby_enabled | boolean | |
feed_visibility | friends · none | |
leaderboard_visibility | username · anonymous · hidden |
Respostas
- 200 Updated
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
PATCH /v1/me/profile Update wellness profile auth
Goal type, activity level, dietary pattern, habits and routine. Feeds goal calculation.
Corpo
| Campo | Tipo | Descrição |
|---|---|---|
goal_type | lose_weight · gain_muscle · sleep_better · more_energy · general_health · maintain | |
activity_level | sedentary · light · moderate · active · very_active | |
target_weight_kg | number | |
weight_pace_kg_week | number | (min 0.1, max 1) |
dietary_pattern | string | |
dietary_restrictions | array<string> | |
usual_wake_time | string | |
usual_bed_time | string | |
caffeine_cups_per_day | integer | |
alcohol_units_per_week | integer |
Respostas
- 200 Updated
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/me/quotas Remaining AI quota auth
Lets the app render "2 of 3 analyses left this week" rather than letting a failed request be how the user discovers a limit.
Free is a weekly allowance; Premium is a daily fair-use ceiling.
Respostas
- 200 Quota state
- 401 Token ausente, inválido, expirado ou revogado.
Exemplo de resposta
{
"data": {
"plan": "free",
"quotas": {
"ai_photo": {
"used": 1,
"limit": 3,
"remaining": 2,
"resets_at": "2026-09-01T00:00:00+00:00",
"period": "week"
}
}
}
}IA de alimentos
4 endpointsFood analysis from photographs and text.
GET /v1/ai/analyses Recent analyses auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
limit | query | string | Itens por página (1–100). |
Respostas
- 200 History
- 401 Token ausente, inválido, expirado ou revogado.
PATCH /v1/ai/analyses/{id} Correct an analysis auth
Records the user's corrected item list.
This is the highest-signal data the product generates: it is precisely where the model was wrong, labelled by someone looking at the actual plate. Send it whenever the user edits a result — it drives prompt and reference-portion tuning.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
itemsobrigatório | array<AnalysisItem> |
Respostas
- 200 Correction recorded
- 404 Recurso inexistente.
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
POST /v1/ai/food/photo Analyse a meal photograph authattest10/60spremiumidempotente
Sends a plate photo for nutritional analysis and returns identified items with estimated portions and macros. Tuned for Brazilian food: the prompt carries reference portions for arroz, feijão, farofa, pão francês, marmita and the rest, because a generic vision model sizes these against American reference portions and gets them badly wrong.
Before the call reaches a model:
ai_photo_processingconsent must be granted.- An App Attest assertion must be present (when attestation is enabled).
- The plan quota is consumed — falling back to purchased credits when the allowance is gone.
- Per-user and global daily spend caps are checked.
Cost behaviour. Identical photos are content-addressed by SHA-256, so a re-send is a free cache hit (cached: true). The fast model runs first; a low-confidence result escalates to the accurate model only while the day's escalation rate is under its cap.
The image never keeps its metadata. It is re-encoded server-side, which strips EXIF — a plate photo will not ship the user's GPS coordinates to the model provider. The file is deleted within 24 hours.
Send X-Idempotency-Key: mobile networks drop responses, and without it a retry pays for the analysis twice.
When the model cannot see food, it returns items: [] with an explanatory note rather than inventing a meal. Render the note; do not treat it as an error.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
X-Idempotency-Key | header | string | Torna a repetição segura. |
Corpo · multipart/form-data · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
photoobrigatório | string | JPEG, PNG, HEIC or WebP. Max 6 MB. Downscaled to 1568px server-side. |
meal_type | breakfast · lunch · dinner · snack | |
hint | string | Optional user note, e.g. "sem arroz". (max 200) |
Respostas
- 201 Analysis complete
- 402 Limite do plano gratuito ou recurso Welleva+.
- 403 Consent missing, or device attestation failed
- 409 Conflito com o estado atual.
- 503 Dependência indisponível ou guardrail de custo acionado.
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"analysis_id": "anl_m8k2p4nq7xw3z9a",
"items": [
{
"name": "Arroz branco cozido",
"grams": 150,
"kcal": 193,
"protein_g": 3.6,
"carbs_g": 42.3,
"fat_g": 0.4,
"confidence": 0.88,
"portion_description": "1 escumadeira grande"
},
{
"name": "Feijão carioca",
"grams": 100,
"kcal": 76,
"protein_g": 4.8,
"carbs_g": 13.6,
"fat_g": 0.5,
"confidence": 0.84,
"portion_description": "1 concha"
},
{
"name": "Bife grelhado",
"grams": 110,
"kcal": 231,
"protein_g": 31.2,
"carbs_g": 0,
"fat_g": 11.4,
"confidence": 0.79,
"portion_description": "1 unidade média"
}
],
"totals": {
"kcal": 500,
"protein_g": 39.6,
"carbs_g": 55.9,
"fat_g": 12.3,
"grams": 360
},
"confidence": 0.84,
"meal_type": "lunch",
"cached": false,
"model": "claude-haiku-4-5"
},
"meta": {
"quota": {
"used": 1,
"limit": 3,
"remaining": 2,
"period": "week",
"source": "plan"
}
}
}POST /v1/ai/food/text Analyse a meal description authattest20/60spremiumidempotente
Parses natural language — "dois ovos mexidos e um pão francês" — into items with macros.
Cheaper and more accurate than photo analysis when the user states quantities, because there is no portion estimation to get wrong. Matching catalogue entries are injected into the prompt so the model anchors on real TACO values rather than its own recollection of them.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
X-Idempotency-Key | header | string | Torna a repetição segura. |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
descriptionobrigatório | string | (min 3, max 500) |
meal_type | breakfast · lunch · dinner · snack |
{
"description": "2 ovos mexidos, um pão francês com manteiga e um café preto",
"meal_type": "breakfast"
} Respostas
- 201 Analysis complete
- 402 Limite do plano gratuito ou recurso Welleva+.
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 503 Dependência indisponível ou guardrail de custo acionado.
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Catálogo nutricional
8 endpointsNutrition database: search, barcode, contributions.
GET /v1/catalog/barcode/{ean} Look up a barcode auth90/60s
Resolves EAN-8/13 or UPC-A. Order: local catalogue → cached lookup → Open Food Facts.
Products OFF does not know are cached as not_found for a week, so repeatedly scanning an unknown item does not generate an outbound request every time. A 404 includes can_contribute: true — prompt the user to add it via POST /v1/catalog/items.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
eanobrigatório | path | string |
Respostas
- 200 Product found
- 404 Not found in any source
- 422 Falha de validação; details.fields lista os campos.
- 401 Token ausente, inválido, expirado ou revogado.
- 429 Limite de requisições excedido.
GET /v1/catalog/favorites Pinned foods auth
Respostas
- 200 Favourites
- 401 Token ausente, inválido, expirado ou revogado.
PUT /v1/catalog/favorites/{id} Pin a food auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 200 Pinned
- 404 Recurso inexistente.
DELETE /v1/catalog/favorites/{id} Unpin a food auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 204 Unpinned
POST /v1/catalog/items Contribute a food item auth60/300s
Adds a missing product. Enters as pending_review: immediately usable by its author, but it does not reach everyone's search results until moderated.
Macros are checked for physical coherence — protein and carbs at 4 kcal/g, fat at 9 — with a 20% tolerance. An inconsistent entry returns 422 macros_inconsistent with the derived figure, so the user can see what is wrong.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
nameobrigatório | string | |
brand | string? | |
barcode | string? | |
category | string? | |
is_liquid | boolean | |
kcal_100obrigatório | number | |
protein_100obrigatório | number | |
carbs_100obrigatório | number | |
fat_100obrigatório | number | |
fiber_100 | number? | |
sodium_mg_100 | number? | |
default_serving_g | number? | |
portions | array<object> |
Respostas
- 201 Submitted for review
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/catalog/items/{id} Get one food item auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 200 Item with portions and micronutrients
- 404 Recurso inexistente.
GET /v1/catalog/recent Recently used foods auth
Server-side, so a new device is immediately useful.
Respostas
- 200 Recent items
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/catalog/search Search foods auth120/60s
Full-text search over TACO (Brazil), USDA and Open Food Facts entries.
Matching blends three signals, because none alone handles Brazilian food vocabulary: unaccented prefix matching ("pao frances" finds "Pão Francês"), a synonym table for regional names ("cinquentinha"), and FULLTEXT relevance. Results are then ranked by how often people actually log them.
Responses are cached for 15 minutes, which absorbs the typing burst of an autocomplete field.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
qobrigatório | query | string | |
category | query | string | |
limit | query | string | Itens por página (1–100). |
Respostas
- 200 Matches
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Sincronização
7 endpointsDaily health aggregates. Raw HealthKit samples never leave the device.
GET /v1/sync/aggregates Download aggregates for a range auth
Restores history to a reinstalled or second device. Maximum 400 days per request.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
from | query | string | |
to | query | string |
Respostas
- 200 Aggregates
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
POST /v1/sync/aggregates Upload daily aggregates auth60/300s
Uploads one row per day of totals. Up to 120 days per request.
This endpoint accepts daily aggregates only. Raw HealthKit samples — heart-rate series, individual sleep sessions, workout GPS routes — must never be sent, and the server ignores any field outside its allow-list rather than storing it. What is stored exists to serve challenges, leagues, Wrapped percentiles and cross-device continuity; nothing else needs a server.
Partial updates are safe. Only the keys present in each day are written, so a device syncing nutrition will not blank out sleep data another device wrote.
Values outside physiological bounds are dropped, not clamped. 9,999,999 steps or a resting heart rate of 300 is a HealthKit glitch or a unit mix-up, and silently correcting it would put a fictional number on a leaderboard.
Streaks are recomputed on every accepted upload, so a watch uploading Tuesday's data on Thursday still extends Tuesday's streak.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
daysobrigatório | array<DailyAggregate> | (máx itens 120) |
device_uuid | string? |
{
"device_uuid": "B8F3…",
"days": [
{
"day": "2026-08-27",
"steps": 11420,
"active_kcal": 612,
"exercise_minutes": 47,
"sleep_minutes": 445,
"sleep_deep_min": 78,
"sleep_rem_min": 96,
"resting_hr": 54,
"hrv_ms": 62.4,
"weight_kg": 73.8,
"kcal_in": 2080,
"protein_g": 138,
"water_ml": 2600,
"meals_logged": 4,
"daily_score": 84,
"sleep_score": 79
}
]
} Respostas
- 202 Accepted
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"accepted": 1,
"days": [
"2026-08-27"
],
"rejected": [
{
"day": "2026-09-05",
"reason": "future_date"
}
],
"streaks": [
{
"streak_type": "all_goals",
"current_length": 12,
"longest_length": 31
}
]
}
}GET /v1/sync/resources Recursos sincronizáveis e o protocolo auth
Lista os domínios que o app pode espelhar na conta, com os campos aceitos por cada um.
Serve para o cliente descobrir a superfície em vez de embutir uma lista que envelhece silenciosamente quando o servidor muda.
Respostas
- 200 Recursos disponíveis
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/sync/status Sync state and insight readiness auth
Reports coverage and how close the account is to the 30 days the Cause & Effect engine needs.
insights_readiness.progress is what drives the "12/30 days collected" cold-start card — it turns a two-week wait into visible progress instead of an empty screen.
Respostas
- 200 Status
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/sync/summary Trends over a window auth
Averages for the last N days against the same-length preceding window, so the comparison is like-for-like. Direction is reported as up/down/flat with a 3% dead band, which keeps day-to-day noise from reading as a trend.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
days | query | integer |
Respostas
- 200 Trends
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/sync/{resource} Buscar alterações de um recurso auth
Devolve tudo que mudou depois do cursor, incluindo exclusões.
Passe meta.next_cursor da resposta anterior em since. O cursor vem do relógio do servidor, com precisão de milissegundo — de propósito. Se ele usasse o updated_at do cliente, um aparelho com o relógio errado gravaria registros que ordenam antes do cursor e ficariam invisíveis para sempre; e se fosse truncado ao segundo, o cliente rebaixaria o mesmo registro em todo poll sem nunca convergir.
Registros excluídos aparecem com deleted_at preenchido em vez de sumirem, para que a exclusão seja aplicada localmente.
Na primeira sincronização, omita since para receber tudo.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
resourceobrigatório | path | fast-sessions · medication-plans · medication-logs · sleep-nights · smart-alarm-sessions · lucid-coach-sessions · readiness-daily · mindfulness-logs | |
since | query | string | Cursor da resposta anterior. Omitir traz tudo. |
limit | query | integer |
Respostas
- 200 Alterações
- 404 Recurso desconhecido
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": [
{
"id": "B8F3E1A0-…",
"started_at": "2026-08-27T21:00:00.000+00:00",
"target_hours": 16,
"updated_at": "2026-08-28T13:00:04.512+00:00",
"deleted_at": null
}
],
"meta": {
"next_cursor": "2026-08-28T13:00:05.117+00:00",
"has_more": false,
"count": 1
}
}POST /v1/sync/{resource} Enviar registros de um recurso auth60/300s
Envia um lote de registros que o aparelho criou ou alterou. Até 500 por requisição.
Idempotência. A chave é usuário + id. Reenviar o mesmo id atualiza o mesmo registro; nunca cria um segundo. Um retry depois de uma resposta perdida é seguro por construção, o que em rede móvel é requisito e não otimização.
Conflito. Vence quem tem o updated_at mais recente — o relógio do cliente, porque um aparelho que ficou offline precisa poder trazer trabalho mais novo. Quando o servidor já tem uma versão igual ou mais nova, o registro volta em skipped: isso é o sinal para o cliente fazer um pull, não para reenviar.
Exclusão. Mande deleted_at. O registro permanece como marcador para que a exclusão chegue aos outros aparelhos; um DELETE de verdade seria invisível para um aparelho que ainda não sincronizou, e o registro ressuscitaria no próximo push dele.
Recursos com chave por dia (sleep-nights, readiness-daily) derivam a identidade do próprio campo day. Dois aparelhos que calcularam a mesma noite convergem numa linha só, em vez de duplicá-la.
Um registro inválido é rejeitado individualmente e devolvido em rejected com os campos que falharam — o resto do lote é aplicado normalmente.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
resourceobrigatório | path | fast-sessions · medication-plans · medication-logs · sleep-nights · smart-alarm-sessions · lucid-coach-sessions · readiness-daily · mindfulness-logs |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
recordsobrigatório | array<object> | (máx itens 500) |
{
"records": [
{
"id": "B8F3E1A0-4C2D-4E1F-9A3B-7C5D2E8F1A6B",
"started_at": "2026-08-27T21:00:00.000Z",
"ended_at": "2026-08-28T13:00:00.000Z",
"target_hours": 16,
"updated_at": "2026-08-28T13:00:04.512Z"
}
]
} Respostas
- 202 Lote processado
- 404 Recurso desconhecido
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"applied": 3,
"skipped": 1,
"rejected": [
{
"id": "…",
"reason": "invalid",
"fields": {
"status": [
"…"
]
}
}
]
},
"meta": {
"server_time": "2026-08-28T13:00:05.117+00:00"
}
}Desafios
8 endpointsGroup challenges with live leaderboards.
GET /v1/challenges Your challenges auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
status | query | upcoming · active · completed |
Respostas
- 200 Challenges
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/challenges Create a challenge auth10/3600spremium
Premium only. Free users can join and compete in challenges friends create — which is what keeps the invite loop working without paywalling participation.
Maximum duration 90 days, maximum 200 participants.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
titleobrigatório | string | (min 3, max 80) |
description | string? | |
metricobrigatório | steps · water_ml · active_minutes · fasting_minutes · sleep_minutes · protein_g · mindful_minutes · daily_goal_hits · workouts | |
mode | total · average · streak · goal_hits | |
target_value | number? | Required for goal_hits and streak modes. |
visibility | private · friends · public | |
max_participants | integer | (min 2, max 200) |
starts_onobrigatório | string | |
ends_onobrigatório | string |
{
"title": "Semana dos 10k passos",
"metric": "steps",
"mode": "total",
"visibility": "friends",
"starts_on": "2026-09-01",
"ends_on": "2026-09-07"
} Respostas
- 201 Created
- 402 Limite do plano gratuito ou recurso Welleva+.
- 409 Conflito com o estado atual.
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/challenges/{id} Challenge detail with leaderboard auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 200 Challenge
- 404 Recurso inexistente.
POST /v1/challenges/{id}/invite Invite people auth60/300s
Owner only. Returns which usernames were invited and which were skipped, with a reason.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
usernamesobrigatório | array<string> | (máx itens 50) |
Respostas
- 200 Invitations processed
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
POST /v1/challenges/{id}/join Join a challenge authpremium
Private challenges need a join_code unless you were invited.
Joining mid-challenge backfills the days already elapsed from your synced aggregates, so a late joiner does not start permanently behind.
Free accounts may hold one active challenge at a time.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo
| Campo | Tipo | Descrição |
|---|---|---|
join_code | string? |
Respostas
- 201 Joined
- 402 Limite do plano gratuito ou recurso Welleva+.
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 409 Already joined, full, or closed
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/challenges/{id}/leaderboard Live leaderboard auth
Ranked by progress. Participants set to anonymous appear without an identity; hidden are omitted entirely.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string | |
limit | query | string | Itens por página (1–100). |
Respostas
- 200 Leaderboard
- 404 Recurso inexistente.
POST /v1/challenges/{id}/leave Leave a challenge auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 204 Left
POST /v1/challenges/{id}/progress Refresh your progress auth
Recomputes your standing from your own synced aggregates. The app asks the server to refresh; it does not tell the server what the number is. That is what makes the leaderboard trustworthy — the same data that drives your dashboard drives your rank.
Call this after a sync if you want the board to update immediately.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 200 Progress refreshed
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Ligas
3 endpointsWeekly consistency leagues.
GET /v1/leagues/current Current league and standings auth
Recomputes your points and returns your weekly cohort.
Leagues rank by consistency points, never by body metrics. Ranking people by weight or body fat rewards being already-lean over improving, which is actively harmful in a wellness app — so the schema does not offer the columns to do it. Points come from logging, hitting your own goals, workouts and mindful sessions, which puts a beginner and an athlete on the same board.
Cohorts hold up to 30 people. Top 7 promote, bottom 5 demote, and members with zero points neither rise nor fall — inactive accounts should not occupy promotion slots or pad the demotion zone.
Tiers: bronze → silver → gold → diamond.
Respostas
- 200 League standings
- 401 Token ausente, inválido, expirado ou revogado.
Exemplo de resposta
{
"data": {
"league": {
"tier": "silver",
"season_week": "2026-W35",
"members": 28,
"promote_top": 7,
"demote_bottom": 5
},
"standings": [
{
"rank": 3,
"user": {
"username": "ana.silva"
},
"points": 284,
"is_you": true,
"zone": "promotion"
}
],
"my_rank": 3,
"my_points": 284
}
}GET /v1/leagues/history Past seasons auth
Up to 52 past weeks with rank and outcome, plus lifetime tier statistics.
Respostas
- 200 History
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/leagues/standings Standings without recomputing auth
Same shape as /current, but does not recompute points. Cheaper for polling.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
limit | query | string | Itens por página (1–100). |
Respostas
- 200 Standings
- 401 Token ausente, inválido, expirado ou revogado.
Nearby
3 endpointsOpt-in discovery using approximate location only.
POST /v1/nearby/checkin Check in to an approximate area auth12/3600spremium
Makes you discoverable to nearby users who want to walk, run or train together.
Coordinates are never stored. They are converted to a geohash at the moment of receipt and discarded: precision 6 (~1.2 km) is kept internally, precision 5 (~4.9 km) is what discovery matches on. That is enough to suggest a walking partner and useless for locating an individual.
There is one active check-in per user, so no location trail can accumulate, and it expires automatically (4 h default, 24 h maximum).
Requires all of: Premium, nearby_enabled in privacy settings, nearby_location consent, and an age of 17+ derived from birth_date. Each is a separate gate with its own error code.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
latitudeobrigatório | number | (min -90, max 90) |
longitudeobrigatório | number | (min -180, max 180) |
activityobrigatório | walk · run · gym · yoga · cycling · other | |
note | string? | (max 140) |
ttl_hours | integer | (min 1, max 24) |
Respostas
- 201 Checked in
- 402 Limite do plano gratuito ou recurso Welleva+.
- 403 Opt-in, consent or age requirement not met
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"checked_in": true,
"activity": "run",
"area": "6gycf",
"precision": "approximately 5 km",
"expires_at": "2026-08-28T15:00:00+00:00"
}
}DELETE /v1/nearby/checkin Check out auth
Removes your check-in immediately, ahead of its expiry.
Respostas
- 204 Checked out
GET /v1/nearby/discover Find nearby users authpremium
Searches your geohash cell plus its eight neighbours — two people 100 m apart can sit either side of a cell boundary, and searching only the exact cell would hide them from each other.
Results carry a coarse distance band (very_close / close / nearby / in_your_area), never a figure in metres: an exact distance from several vantage points would trilaterate someone's position.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
activity | query | walk · run · gym · yoga · cycling · other |
Respostas
- 200 Nearby users
- 402 Limite do plano gratuito ou recurso Welleva+.
- 403 Not checked in, or requirements not met
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Wrapped
2 endpointsMonthly and annual health recaps with population percentiles.
GET /v1/wrapped Available Wrapped periods auth
Respostas
- 200 Periods
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/wrapped/{period} Get a Wrapped report auth
period is 2026 for a year or 2026-08 for a month.
The backend returns numbers and percentiles; the app renders the cards natively, which is what keeps them fast and shareable at full resolution.
Percentiles come from pre-computed population cut-points, so "you walked more than 78% of users" is a single lookup that never touches another user's row. A percentile is withheld entirely when the sample is under 100 people — a rank drawn from a handful of users is noise, and can be de-anonymising in a small cohort.
Free accounts receive the headline numbers with a watermark; Premium receives the full set, percentiles and theme options.
Returns 404 wrapped_insufficient_data below 10 days (month) or 60 days (year) of data.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
periodobrigatório | path | string |
Respostas
- 200 Wrapped report
- 404 Not enough data for this period
- 422 Falha de validação; details.fields lista os campos.
Exemplo de resposta
{
"data": {
"period_type": "month",
"period_key": "2026-08",
"days_covered": 28,
"is_full": true,
"stats": {
"total_steps": 284120,
"total_distance_km": 213.4,
"workouts": 18,
"water_litres": 71.2,
"longest_streak": 22,
"avg_sleep_hours": 7.3,
"fun_facts": [
{
"key": "distance",
"value": 213.4,
"unit": "km",
"equivalent": "5.1 maratonas"
}
]
},
"percentiles": {
"steps": {
"value": 284120,
"percentile": 78,
"better_than": "78%"
}
},
"shareable": {
"formats": [
"story_9_16",
"square_1_1"
],
"watermark": false,
"themes": [
"dark",
"light",
"gradient",
"minimal"
]
}
}
}Assinaturas
5 endpointsSubscriptions, entitlements and App Store notifications.
GET /v1/billing/credits AI credit balance and ledger auth
Consumable credits from packs. Spent automatically once the plan allowance runs out.
Respostas
- 200 Balance and recent ledger
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/billing/link Link a StoreKit purchase to this account authidempotente
Call right after a successful StoreKit purchase so entitlement appears without waiting for Apple's notification.
This is a fast path, not a trust boundary. If the signed notification has not arrived yet, the response is 202 with linked: false and no entitlement is granted — granting on an unverified client claim would make the whole signature check pointless.
Set appAccountToken to the user's public_id when starting the StoreKit purchase; that is what lets Apple's notification be attributed to an account at all.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
X-Idempotency-Key | header | string | Torna a repetição segura. |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
original_transaction_idobrigatório | string | |
product_idobrigatório | string |
Respostas
- 200 Linked; entitlement active
- 202 Acknowledged; awaiting Apple's notification
- 409 That purchase belongs to another account
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/billing/products Product catalogue and plan comparison público
Product identifiers plus the Free/Premium capability matrix, for rendering a paywall.
Prices are reference values only — StoreKit price tiers are authoritative for the user's storefront.
Respostas
- 200 Catalogue
GET /v1/billing/subscription Current subscription and entitlement auth
The server's view of what the user is entitled to, plus their AI credit balance.
Respostas
- 200 Subscription state
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/billing/webhook App Store Server Notifications v2 público
Apple's webhook. Not called by the app.
Unauthenticated by necessity — Apple cannot present a bearer token. The JWS signature is the authentication: the payload's certificate chain is verified against Apple's Root CA G3, every certificate's validity window is checked, and only then is the ES256 signature verified. An unverifiable payload is rejected outright, never processed "just in case".
Processing is idempotent (notificationUUID is unique, so Apple's retries cannot double-grant) and the raw payload is stored before processing, so a bug in the handler is replayable rather than a permanently lost billing event.
Always answers 200 once the signature checks out. A non-2xx makes Apple retry, and retrying a payload the handler cannot process just repeats the failure — the stored row is the recovery path.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
signedPayloadobrigatório | string | The JWS Apple sends. |
Respostas
- 200 Handled (processed, duplicate, ignored or recorded-as-failed)
- 400 Missing signedPayload
- 401 Signature or certificate chain verification failed
Configuração remota
2 endpointsSigned remote configuration and experiments.
GET /v1/config Remote configuration público600/60s
Feature flags, kill-switches, score weights, free/premium limits and experiment assignments.
Works without a session — a cold start needs flags before it has an account. Signed in, the response additionally reflects the caller's plan and their experiment variants.
Signed with ES256. Verify signature.token against the public key shipped in the app before applying anything: without it, a hostile network could rewrite score weights or flip the AI kill-switches on the wire.
Cache for 60 seconds. A kill-switch has to take effect within about a minute, and this endpoint is hit on every cold start.
Experiment assignment is sticky: it is a hash of the user id and experiment key, so a user lands in the same variant on every device and after every reinstall.
Respostas
- 200 Configuration
GET /v1/config/keys Chave pública e vetor de teste da assinatura público600/60s
Tudo o que um cliente precisa para verificar a assinatura de GET /v1/config.
A assinatura é feita sobre o corpo canonicalizado pela RFC 8785 (JSON Canonicalization Scheme), não sobre a saída bruta do servidor. Sem isso um cliente Swift não teria como reproduzir os mesmos bytes — o json_encode do PHP emite chaves na ordem de inserção — e a assinatura seria impossível de conferir na prática.
Como verificar
- Remova o objeto
signaturedo corpo deGET /v1/config. - Canonicalize o restante pela RFC 8785.
- Calcule SHA-256 desses bytes, em hexadecimal minúsculo.
- Verifique o JWS de
signature.tokencom a chave cujokidbate com o do cabeçalho. - Confirme que a claim
digestdo JWS é igual ao hash calculado. - Rejeite se
expjá passou.
O test_vector traz uma entrada com chaves fora de ordem, sua forma canônica e o digest correspondente. Se a sua implementação reproduz aquela string exata, ela está correta — e isso separa "meu canonicalizador está errado" de "a assinatura é realmente inválida", que é a diferença entre depurar e desistir da verificação.
O kid é o thumbprint RFC 7638 da chave, então rotacionar é publicar uma segunda entrada, não coordenar um release.
Respostas
- 200 Material de verificação
- 503 Nenhuma chave de assinatura instalada neste servidor
Exemplo de resposta
{
"data": {
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "…",
"y": "…",
"alg": "ES256",
"use": "sig",
"kid": "…"
}
],
"verification": {
"algorithm": "ES256",
"canonicalization": "RFC 8785 (JSON Canonicalization Scheme)"
},
"test_vector": {
"canonical_json": "{\"config\":{…},\"version\":7}",
"canonical_sha256": "…"
}
}
}Telemetria
1 endpointProduct analytics ingestion.
POST /v1/telemetry/events Send a batch of product events público30/60s
Batched analytics. Up to 100 events per request; works with or without a session.
Two rules keep this from becoming a privacy problem. Event names are allow-listed — an unknown name is dropped and reported back in meta.unknown_events, so a client bug is debuggable rather than silently losing a release's analytics. And properties are scalars only, capped at 20 keys and 200 characters, so health data cannot be smuggled in as an "event property".
Client clocks drift, so occurred_at is clamped: anything more than 7 days old or an hour in the future is replaced with server time.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
eventsobrigatório | array<object> | (máx itens 100) |
session_id | string | |
device_id | string | |
app_version | string | |
os_version | string |
{
"session_id": "s_9f2a",
"app_version": "1.0.0",
"events": [
{
"name": "paywall_viewed",
"properties": {
"variant": "annual_highlight",
"trigger": "ai_limit"
}
},
{
"name": "meal_logged",
"properties": {
"method": "photo"
}
}
]
} Respostas
- 202 Accepted
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"accepted": 2,
"dropped": 0,
"unknown_events": []
}
}Notificações
5 endpointsAPNs tokens and notification preferences.
POST /v1/push/opened Report a notification open auth
Attribution for notification effectiveness. Call from the notification-response handler.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
rule_keyobrigatório | string |
Respostas
- 204 Recorded
GET /v1/push/preferences Notification preferences auth
Per-category toggles and quiet hours. Quiet hours are evaluated in the user's timezone.
Respostas
- 200 Preferences
- 401 Token ausente, inválido, expirado ou revogado.
PATCH /v1/push/preferences Update notification preferences auth
Categories: functional (reminders the user asked for), engagement, conversion, social, system.
Windows crossing midnight (22:00–07:30) are handled correctly. Getting this wrong is how a wellness app wakes someone at 3 a.m.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
preferencesobrigatório | object |
{
"preferences": {
"conversion": {
"enabled": false
},
"functional": {
"quiet_start": "23:00",
"quiet_end": "06:30"
}
}
} Respostas
- 200 Updated
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
PUT /v1/push/token Register an APNs device token auth
Call on every launch — APNs tokens rotate. A token APNs later reports as unregistered is forgotten automatically.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
device_uuidobrigatório | string | |
push_tokenobrigatório | string | |
environment | sandbox · production | |
platform | ios · watchos · ipados | |
app_version | string? | |
os_version | string? | |
model | string? |
Respostas
- 200 Registered
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
DELETE /v1/push/token Unregister a device token auth
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
device_uuidobrigatório | string |
Respostas
- 204 Unregistered
Privacidade (LGPD)
7 endpointsLGPD/GDPR data export and account deletion.
DELETE /v1/privacy/account Schedule account deletion auth
LGPD art. 18 §VI. Two-phase by design.
Now: the account is marked pending deletion and every session is revoked — the account stops working the moment deletion is requested, not when it executes.
After the grace window (14 days): personal data is deleted outright and the account row is anonymised. Anonymising rather than dropping keeps aggregate statistics and legally-retained billing records valid while removing every identifier. There is no path back from a completed deletion.
Requires confirm: true. An irreversible action should need an explicit acknowledgement, not just the right HTTP verb.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
confirmobrigatório | true | |
reason | string? |
Respostas
- 202 Deletion scheduled
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
POST /v1/privacy/account/cancel-deletion Cancel a scheduled deletion auth
Reactivates the account within the grace window. Requires signing in again, since sessions were revoked.
Respostas
- 200 Cancelled
- 409 Nothing scheduled
GET /v1/privacy/account/deletion-status Deletion status auth
Respostas
- 200 Status, or
{"status":"none"} - 401 Token ausente, inválido, expirado ou revogado.
POST /v1/privacy/export Request a data export auth3/86400s
LGPD art. 18 / GDPR art. 15. Builds a complete machine-readable archive of everything the server holds.
Built asynchronously; poll GET /v1/privacy/export/{id}. The archive expires after 7 days. Limited to 3 requests per day.
Corpo
| Campo | Tipo | Descrição |
|---|---|---|
format | json · csv |
Respostas
- 202 Export queued
- 401 Token ausente, inválido, expirado ou revogado.
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
GET /v1/privacy/export/{id} Export status auth
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 200 Status
- 404 Recurso inexistente.
GET /v1/privacy/export/{id}/download Download an export auth
Requires the one-time token issued when the export completed. Returns the archive as a file attachment.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string | |
tokenobrigatório | query | string |
Respostas
- 200 The archive
- 404 Not ready, expired, or the token does not match
GET /v1/privacy/summary What is stored, and what is not auth
A plain-language statement for the in-app privacy screen. An endpoint rather than a static page, so the app shows the real answer for this build rather than a generic policy.
Respostas
- 200 Privacy summary
- 401 Token ausente, inválido, expirado ou revogado.
Lista de espera
2 endpointsPre-launch waitlist for the landing page.
POST /v1/waitlist Join the pre-launch waitlist público8/3600s
Captures an email from the marketing site before the App Store listing is live.
Unauthenticated by necessity — the visitor has no account yet. Three defences stand in for auth: a honeypot field (website, rendered off-screen), a minimum time-on-form (elapsed, since bots submit instantly), and per-IP rate limiting.
Re-submitting an address returns the same success response as a first signup. Answering "that email is already registered" would turn this into a membership oracle for any address someone cares to try.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
emailobrigatório | string | |
name | string? | (max 80) |
goal | lose_weight · gain_muscle · sleep_better · more_energy · general_health? | |
locale | string? | |
website | string? | Honeypot. Must stay empty. |
elapsed | integer? | Seconds the form was on screen. |
utm_source | string? | |
utm_medium | string? | |
utm_campaign | string? |
{
"email": "ana@exemplo.com",
"name": "Ana",
"goal": "sleep_better",
"elapsed": 14
} Respostas
- 201 On the list
- 422 Falha de validação; details.fields lista os campos.
- 429 Limite de requisições excedido.
Exemplo de resposta
{
"data": {
"joined": true,
"position": 428,
"message": "Você está na lista."
}
}GET /v1/waitlist/count Waitlist size público
Public count for social proof on the landing page. Cached for 5 minutes.
Respostas
- 200 Current count
Exemplo de resposta
{
"data": {
"count": 428
}
}Backoffice
20 endpointsConsole de operação: identidade própria, sessão por cookie e 2FA obrigatório.
POST /v1/admin/auth/login Etapa de senha público10/900s
Primeira das duas etapas. Emite um cookie de sessão que não libera nada até o TOTP passar, então uma senha roubada sozinha não alcança rota alguma.
Conta inexistente e senha errada devolvem exatamente a mesma resposta, para que o endpoint não sirva de lista de operadores.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
emailobrigatório | string | |
passwordobrigatório | string |
Respostas
- 200 Senha aceita; falta o segundo fator
- 401 Credenciais inválidas
- 403 Conta de operador suspensa
- 429 Limite de requisições excedido.
POST /v1/admin/auth/logout Encerrar a sessão público
Respostas
- 204 Sessão encerrada
GET /v1/admin/auth/me Operador atual, permissões e token CSRF público
O painel chama isto ao carregar: estabelece se há sessão viva e entrega o token CSRF das escritas.
Respostas
- 200 Contexto do operador
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/admin/auth/mfa Etapa do segundo fator público12/900s
Aceita um código TOTP ou um código de recuperação de uso único. Só aqui a sessão passa a valer.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
codeobrigatório | string |
Respostas
- 200 Autenticado
- 401 Código inválido ou sessão expirada
- 429 Limite de requisições excedido.
POST /v1/admin/auth/mfa/confirm Confirmar o 2FA e receber os códigos de recuperação público12/900s
Os códigos são exibidos uma única vez e cada um vale um uso.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
codeobrigatório | string | (min 6, max 6) |
Respostas
- 201 2FA ativo
- 401 Código inválido
POST /v1/admin/auth/mfa/setup Iniciar cadastro do 2FA público12/900s
Gera um segredo e a URI otpauth://. O segredo fica inutilizável até ser confirmado.
Respostas
- 200 Segredo e URI
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/admin/auth/password Trocar a senha público10/900s
Encerra todas as outras sessões — que é justamente o motivo de trocar a senha depois de uma suspeita.
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
current_passwordobrigatório | string | |
new_passwordobrigatório | string | (min 12) |
Respostas
- 200 Senha alterada
- 401 Senha atual incorreta
GET /v1/admin/auth/sessions Sessões ativas do operador público
Respostas
- 200 Sessões
- 401 Token ausente, inválido, expirado ou revogado.
POST /v1/admin/auth/sessions/revoke-others Encerrar as outras sessões público
Respostas
- 200 Sessões encerradas
- 401 Token ausente, inválido, expirado ou revogado.
GET /v1/admin/growth/ai-cost Custo de IA por dia, recurso e modelo público
Respostas
- 200 Custo
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/growth/retention Retenção D1/D7/D30 por coorte semanal público
Respostas
- 200 Coortes
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/growth/revenue Receita, MRR e conversão de trial público
O MRR conta apenas assinaturas pagantes. Incluir trials inflaria o número sem nenhuma receita por trás, e a conversão só considera trials que já tiveram tempo de se resolver.
Respostas
- 200 Receita
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/growth/waitlist Lista de espera, com origem e objetivo público
Respostas
- 200 Cadastros
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/growth/waitlist/export Exportar a lista de espera em CSV público
A exportação é auditada: é uma leitura em massa de dados pessoais e deve deixar rastro.
Respostas
- 200 CSV
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/users Listar e buscar contas público
Busca por username, nome, e-mail ou id público — o que o operador tiver em mãos no atendimento.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
q | query | string | |
plan | query | free · premium · lifetime | |
status | query | active · suspended · pending_deletion | |
limit | query | string | Itens por página (1–100). |
Respostas
- 200 Contas
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/users/{id} Detalhe de uma conta público
Expõe o resumo agregado que um atendimento precisa — plano, cobertura, sequências, assinatura — e nunca o histórico de saúde dia a dia.
Cada leitura é registrada na auditoria: abrir o histórico de alguém é, em si, um ato auditável.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Respostas
- 200 Conta
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 404 Recurso inexistente.
POST /v1/admin/users/{id}/entitlement Conceder ou remover plano manualmente público120/60s
Gravado com source = admin, para nunca ser confundido com uma compra real da App Store nos números de receita.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
planobrigatório | free · premium · lifetime | |
days | integer? | |
reasonobrigatório | string | (min 3) |
Respostas
- 200 Plano atualizado
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
POST /v1/admin/users/{id}/notes Anotação interna sobre a conta público
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
noteobrigatório | string | (max 2000) |
Respostas
- 201 Anotação salva
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
DELETE /v1/admin/users/{id}/notes/{noteId} Remover uma anotação público
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string | |
noteIdobrigatório | path | integer |
Respostas
- 204 Removida
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
PATCH /v1/admin/users/{id}/status Suspender ou reativar uma conta público120/60s
Suspender encerra as sessões imediatamente, não quando os tokens expirarem.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
statusobrigatório | active · suspended | |
reason | string? |
Respostas
- 200 Status alterado
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
Administração
13 endpointsInternal operations: margin, moderation, configuration.
GET /v1/admin/audit Audit log público
Security- and privacy-relevant actions: config changes, suspensions, deletions, token reuse.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
action | query | string | |
limit | query | string | Itens por página (1–100). |
Respostas
- 200 Audit entries
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/catalog/pending User-contributed foods awaiting review público
Respostas
- 200 Pending items
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
PATCH /v1/admin/catalog/{id} Approve or reject a contributed food público
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
statusobrigatório | active · rejected · merged | |
verified | boolean |
Respostas
- 200 Moderated
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/config List remote-config entries público
Respostas
- 200 Entries
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
PUT /v1/admin/config/{key} Set a config value público
Supports percentage rollout and audience segmentation. Every change is written to the audit log.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
keyobrigatório | path | string |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
valueobrigatório | any | Any JSON value. |
type | bool · int · float · string · json | |
description | string? | |
enabled | boolean | |
rollout_percent | integer | (min 0, max 100) |
is_public | boolean | False keeps the value server-side; it is never sent to the app. |
min_app_version | string? |
Respostas
- 200 Updated
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
DELETE /v1/admin/config/{key} Delete a config entry público
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
keyobrigatório | path | string |
Respostas
- 204 Deleted
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/cost/top-spenders Highest AI spend per user público
The fair-use watch list: who is at the top of the cost distribution on a given day.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
day | query | string |
Respostas
- 200 Top spenders
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/funnel Onboarding and conversion funnel público
Unique users at each step from app_opened through purchase_completed, with step-to-step conversion.
Respostas
- 200 Funnel
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/margin Contribution margin report público
Net revenue after Apple's commission and tax, minus AI spend — the measurement behind the ≥40% floor.
margin.below_floor is what the daily watchdog alerts on.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
from | query | string | |
to | query | string |
Respostas
- 200 Margin report
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/metrics Platform metrics público
DAU/WAU/MAU, conversion, AI cost and API health. Requires an admin account.
Respostas
- 200 Metrics
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/notifications/stats Notification delivery and open rates by rule and variant público
Respostas
- 200 Stats
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
GET /v1/admin/reports Moderation queue público
Open user reports, ordered by how many times the reported account has been reported.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
status | query | open · reviewing · actioned · dismissed |
Respostas
- 200 Reports
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
PATCH /v1/admin/reports/{id} Resolve a report público
action: suspend_user suspends the reported account and revokes all its sessions immediately.
Parâmetros
| Nome | Local | Tipo | Descrição |
|---|---|---|---|
idobrigatório | path | integer |
Corpo · obrigatório
| Campo | Tipo | Descrição |
|---|---|---|
statusobrigatório | reviewing · actioned · dismissed | |
note | string? | |
action | none · suspend_user |
Respostas
- 200 Resolved
- 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).
- 404 Recurso inexistente.
CLI e cron
O que roda fora do ciclo de requisição.
| Comando | Frequência | Faz o quê |
|---|---|---|
ai:purge-photos | 15 min | Apaga fotos de prato vencidas, check-ins do Nearby e chaves expiradas. É o que torna a promessa de 24 h real. |
privacy:process | 10 min | Monta exports LGPD e executa exclusões de conta vencidas. |
challenges:finalize | horária | Ativa desafios que começaram e encerra os que terminaram. |
push:dispatch | horária | Streak em risco, fim de trial, inatividade. |
billing:reconcile | horária | Expira entitlements que a Apple nunca notificou. |
maintenance:cleanup | diária 03:30 | Sessões, retenção de analytics, logs. |
stats:percentiles | diária 03:45 | Recalcula pontos de corte populacionais do Wrapped. |
cost:report | diária 08:00 | Margem de contribuição. Sai com código ≠ 0 abaixo do piso, então o cron notifica. |
leagues:rollover | segunda 00:05 | Fecha as ligas da semana, aplica promoções e rebaixamentos. |
wrapped:generate | dia 1º, 05:00 | Pré-gera o Wrapped do mês anterior. |
Comandos de manutenção
php bin/console migrate # aplica migrations pendentes
php bin/console migrate:status # o que rodou, e se algum arquivo divergiu
php bin/console db:check # MariaDB, Redis e analytics
php bin/console routes # tabela de rotas com middleware
php bin/console openapi:generate # regenera a spec — falha se divergir das rotas
php bin/console config:seed # regras de notificação, flags, produtos
php bin/console catalog:import --file=taco.csv --source=taco
Checklist de integração iOS
Ordem de implementação sugerida no app.
- Buscar
/v1/configno cold start e verificar a assinatura ES256 antes de aplicar. Cache de 60 s. - Sign in with Apple com nonce. Persistir o e-mail na primeira autorização — não virá de novo.
- Definir
appAccountToken=user.public_idao iniciar qualquer compra StoreKit. É o que permite atribuir a notificação da Apple a uma conta. - Registrar o token APNs a cada launch — tokens rotacionam.
- Enrolar App Attest uma vez por instalação, antes da primeira análise de IA.
- Mandar
X-Idempotency-Keyem toda chamada de IA e embilling/link. - Sincronizar agregados a cada hora (ou no
interval_minutesda config), em lotes de até 120 dias. - Ler
/v1/me/quotaspara mostrar “2 de 3 análises restantes” — não deixe o usuário descobrir o limite por uma requisição que falha. - Mapear
details.paywalldo 402 para o paywall contextual correto. - Tratar 402, 403 e 503 como estados de UI, não como erros genéricos: cada um tem um caminho de saída específico em
details.
Pendências de credencial antes do lançamento
A API roda sem elas; os recursos que elas destravam, não.
ANTHROPIC_API_KEY (IA), APPLE_TEAM_ID e
APPLE_BUNDLE_ID (Sign in with Apple, App Attest, APNs),
as chaves da App Store Server API (webhook de billing) e as do APNs (push).
Ative APP_ATTEST_ENABLED só depois do Team ID — ligar sem ele
rejeita todos os dispositivos.
Social
14 endpointsFriends, blocks, reports and the achievement feed.
GET /v1/social/blocks List blocked users auth
Respostas
POST /v1/social/blocks Block a user auth60/300s
Blocking is symmetric for visibility and severs any existing friendship and pending request in the same operation.
Corpo · obrigatório
user_idobrigatórioreasonRespostas
DELETE /v1/social/blocks/{id} Unblock a user auth
Parâmetros
idobrigatórioRespostas
GET /v1/social/feed Friend achievement feed auth
Cursor-paginated, newest first. Blocked users are filtered in both directions.
Reactions only, no free-text comments in v1 — a closed reaction set cannot carry harassment, which is what makes social features safe to launch with a small team.
Parâmetros
limitcursorRespostas
DELETE /v1/social/feed/{id} Delete your own post auth
Parâmetros
idobrigatórioRespostas
POST /v1/social/feed/{id}/react React to a post auth60/300s
Sending the same reaction again removes it (toggle). A different one replaces it.
Parâmetros
idobrigatórioCorpo · obrigatório
reactionobrigatórioRespostas
GET /v1/social/friends List friends auth
Each friend's shared metrics honour their privacy settings, applied in SQL — a metric they have not chosen to share is simply absent from
shared, never null-but-present.Respostas
Exemplo de resposta
POST /v1/social/friends Send a friend request auth30/3600s
If the other person already has a pending request to you, this accepts immediately rather than creating a second one — both people clearly want it.
A user who has turned off
discoverable_by_usernamereturns the same404 user_not_foundas a user who does not exist. Two different responses would make the setting probeable.Corpo · obrigatório
usernameobrigatóriosourceRespostas
GET /v1/social/friends/requests Pending friend requests auth
Respostas
POST /v1/social/friends/requests/{id}/accept Accept a friend request auth
Parâmetros
idobrigatórioRespostas
POST /v1/social/friends/requests/{id}/decline Decline a friend request auth
Parâmetros
idobrigatórioRespostas
DELETE /v1/social/friends/{id} Remove a friend auth
Parâmetros
idobrigatórioRespostas
POST /v1/social/reports Report a user auth10/86400s
Queues the report for moderation. Rate limited to 10 per day to keep reporting from becoming a harassment vector itself.
Corpo · obrigatório
user_idobrigatóriocontextobrigatórioreasonobrigatóriodetailsRespostas
GET /v1/social/users/{username} Public profile auth
A blocked user is indistinguishable from a non-existent one: both return 404.
Parâmetros
usernameobrigatórioRespostas