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

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çalhoDireçãoPara quê
Authorization: Bearer …envioAccess token. Obrigatório em rotas autenticadas.
X-Welleva-Localeenviopt-BR, en ou es. Sobrepõe o locale da conta.
X-Welleva-App-VersionenvioBuild do cliente. Alimenta segmentação de config e analytics.
X-Idempotency-KeyenvioTorna a repetição segura. Veja abaixo.
X-Attest-Key-Id
X-Attest-Assertion
X-Attest-Client-Data
envioAsserção App Attest nas rotas de IA.
X-Request-IdambosCorrelação. Ecoado na resposta e presente em todo log da requisição.
X-RateLimit-RemainingrespostaRequisições restantes no bucket.
Retry-AfterrespostaSegundos 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.

StatusSignificadoO que o app deve fazer
400Requisição malformada (JSON inválido)Bug de cliente. Logar e reportar.
401Token ausente, expirado, revogado ou reusadotoken_expired → renovar. Qualquer outro → voltar ao login.
402Limite do plano free ou recurso Welleva+Abrir o paywall que details.paywall indica.
403Autenticado, mas sem permissãoConsentimento, opt-in ou atestação faltando. details.next diz o caminho.
404Recurso inexistenteTambém retornado quando o recurso existe mas o chamador não pode saber disso.
405Método HTTP erradoO cabeçalho Allow lista os válidos.
409Conflito com o estado atualDuplicado, já ingressou, chave de idempotência reusada.
422Falha de validaçãodetails.fields mapeia campo → mensagens. Exibir inline.
429Limite de requisiçõesRespeitar Retry-After. Não retentar em loop.
500Erro inesperadoCitar request_id no suporte.
503Dependência fora, ou guardrail de custo acionadoRespeitar Retry-After. Oferecer registro manual.

Códigos de erro por domínio

CódigoStatusQuando acontece
token_missing401Sem cabeçalho Authorization.
token_expired401Access token venceu. Renove.
token_reuse_detected401Refresh token já rotacionado. Família revogada — refaça login.
session_revoked401Logout, exclusão de conta ou suspensão.
account_suspended403Conta suspensa por moderação.
premium_required402Recurso ou limite Welleva+. Veja details.paywall.
consent_required403Falta consentimento. details.consent_type diz qual.
onboarding_required403Recurso depende do plano gerado no onboarding.
attestation_required403Rota de IA sem asserção App Attest.
attestation_replay403Asserção já usada. Gere uma nova por requisição.
ai_daily_budget_reached503Teto diário de gasto do usuário. Reseta à meia-noite.
ai_global_budget_reached503Teto global acionado. Ofereça registro manual.
ai_feature_disabled503Kill-switch ativo via configuração remota.
ai_unparseable_response503Modelo devolveu algo inesperado. Retentável.
macros_inconsistent422Calorias não batem com os macros informados.
nearby_age_restricted403Nearby exige 17 anos ou mais.
wrapped_insufficient_data404Menos de 10 dias (mês) ou 60 (ano) de dados.
idempotency_key_reused409Mesma chave, corpo diferente.
username_taken409Username 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 armazenadoPor quê
Séries de frequência cardíaca, sessões de sono, rotas de treinoNada no servidor precisa disso.
Coordenadas GPSNearby guarda um geohash de ~5 km e descarta lat/long na hora.
Fotos de progressoFicam no dispositivo.
Fotos de prato além de 24 hProcessadas, EXIF removido, apagadas por cron.
O sub da Apple em claroGuardado 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.

RecursoFreeWelleva+
Análise de foto por IA3 / semana30 / dia (fair use)
Análise por texto5 / semana60 / dia
Busca no catálogo e barcodeilimitadoilimitado
Breakdown dos scores—completo
Body Age e Digital Twin—sim
Cause & Effect (padrões)—sim
Metas adaptativas—sim
Desafios ativos120
Criar desafios—10
Nearby—sim
Wrappedresumido, com marca d'águacompleto e personalizável
Exportar dados (CSV/PDF)—10 / mês
Fotos de progresso1 / mês500 / 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 endpoints

Liveness 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 endpoints

Sign 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
CampoTipoDescrição
identity_tokenobrigatóriostringThe JWS from ASAuthorizationAppleIDCredential.identityToken.
noncestring?The raw nonce whose SHA-256 you passed to Apple.
full_namestring?Also first-authorisation only.
deviceobject
{
    "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
CampoTipoDescrição
refresh_tokenobrigatóriostring
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
NomeLocalTipoDescrição
idobrigatóriopathinteger
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
CampoTipoDescrição
usernameobrigatóriostring
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
NomeLocalTipoDescrição
usernameobrigatórioquerystring
Respostas
  • 200 Availability
  • 401 Token ausente, inválido, expirado ou revogado.

Atestação

2 endpoints

App 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
CampoTipoDescrição
device_uuidobrigatóriostring
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
CampoTipoDescrição
device_uuidobrigatóriostring
key_idobrigatóriostringBase64 key identifier from generateKey.
attestation_objectobrigatóriostringBase64 CBOR attestation object.
challengeobrigatóriostringThe 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 endpoints

Account, 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
CampoTipoDescrição
display_namestring?(max 80)
localept-BR · en · es
timezonestringIANA identifier. Drives quiet hours and daily rollovers.
unit_systemmetric · imperial
birth_datestring
sexfemale · male · other · undisclosed
height_cmnumber(min 120, max 230)
countrystring(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
CampoTipoDescrição
consent_typeobrigatórioterms · privacy · health_processing · ai_photo_processing · social_sharing · nearby_location · marketing_push · analytics
grantedobrigatórioboolean
versionobrigatóriostring
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
CampoTipoDescrição
goalsobrigatórioarray<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
NomeLocalTipoDescrição
metricobrigatóriopathstring
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
CampoTipoDescrição
goal_typeobrigatóriolose_weight · gain_muscle · sleep_better · more_energy · general_health · maintain
sexobrigatóriofemale · male · other · undisclosed
birth_dateobrigatóriostring
height_cmobrigatórionumber(min 120, max 230)
current_weight_kgobrigatórionumber(min 35, max 300)
target_weight_kgnumber?
weight_pace_kg_weeknumber(min 0.1, max 1)
activity_levelobrigatóriosedentary · light · moderate · active · very_active
dietary_patternstring?
dietary_restrictionsarray<string>
measured_tdeenumber?Measured daily energy expenditure from HealthKit. Overrides the estimate.
baseline_sleep_minutesinteger?
baseline_water_mlinteger?
caffeine_cups_per_dayinteger?
{
    "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
CampoTipoDescrição
share_scoresboolean
share_streaksboolean
share_activityboolean
share_nutritionboolean
share_weightboolean
discoverable_by_usernameboolean
nearby_enabledboolean
feed_visibilityfriends · none
leaderboard_visibilityusername · 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
CampoTipoDescrição
goal_typelose_weight · gain_muscle · sleep_better · more_energy · general_health · maintain
activity_levelsedentary · light · moderate · active · very_active
target_weight_kgnumber
weight_pace_kg_weeknumber(min 0.1, max 1)
dietary_patternstring
dietary_restrictionsarray<string>
usual_wake_timestring
usual_bed_timestring
caffeine_cups_per_dayinteger
alcohol_units_per_weekinteger
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 endpoints

Food analysis from photographs and text.

GET /v1/ai/analyses Recent analyses auth
Parâmetros
NomeLocalTipoDescrição
limitquerystringItens 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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
itemsobrigatórioarray<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 authattestconsent: ai_photo_processing10/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:

  1. ai_photo_processing consent must be granted.
  2. An App Attest assertion must be present (when attestation is enabled).
  3. The plan quota is consumed — falling back to purchased credits when the allowance is gone.
  4. 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
NomeLocalTipoDescrição
X-Idempotency-KeyheaderstringTorna a repetição segura.
Corpo · multipart/form-data · obrigatório
CampoTipoDescrição
photoobrigatóriostringJPEG, PNG, HEIC or WebP. Max 6 MB. Downscaled to 1568px server-side.
meal_typebreakfast · lunch · dinner · snack
hintstringOptional 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
NomeLocalTipoDescrição
X-Idempotency-KeyheaderstringTorna a repetição segura.
Corpo · obrigatório
CampoTipoDescrição
descriptionobrigatóriostring(min 3, max 500)
meal_typebreakfast · 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 endpoints

Nutrition 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
NomeLocalTipoDescrição
eanobrigatóriopathstring
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
NomeLocalTipoDescrição
idobrigatóriopathstring
Respostas
  • 200 Pinned
  • 404 Recurso inexistente.
DELETE /v1/catalog/favorites/{id} Unpin a food auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathstring
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
CampoTipoDescrição
nameobrigatóriostring
brandstring?
barcodestring?
categorystring?
is_liquidboolean
kcal_100obrigatórionumber
protein_100obrigatórionumber
carbs_100obrigatórionumber
fat_100obrigatórionumber
fiber_100number?
sodium_mg_100number?
default_serving_gnumber?
portionsarray<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
NomeLocalTipoDescrição
idobrigatóriopathstring
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.

Sincronização

7 endpoints

Daily 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
NomeLocalTipoDescrição
fromquerystring
toquerystring
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
CampoTipoDescrição
daysobrigatórioarray<DailyAggregate>(máx itens 120)
device_uuidstring?
{
    "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
NomeLocalTipoDescrição
daysqueryinteger
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
NomeLocalTipoDescrição
resourceobrigatóriopathfast-sessions · medication-plans · medication-logs · sleep-nights · smart-alarm-sessions · lucid-coach-sessions · readiness-daily · mindfulness-logs
sincequerystringCursor da resposta anterior. Omitir traz tudo.
limitqueryinteger
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
NomeLocalTipoDescrição
resourceobrigatóriopathfast-sessions · medication-plans · medication-logs · sleep-nights · smart-alarm-sessions · lucid-coach-sessions · readiness-daily · mindfulness-logs
Corpo · obrigatório
CampoTipoDescrição
recordsobrigatórioarray<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"
    }
}

Social

14 endpoints

Friends, blocks, reports and the achievement feed.

GET /v1/social/blocks List blocked users auth
Respostas
  • 200 Blocked users
  • 401 Token ausente, inválido, expirado ou revogado.
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
CampoTipoDescrição
user_idobrigatóriostring
reasonstring?
Respostas
  • 201 Blocked
  • 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.
DELETE /v1/social/blocks/{id} Unblock a user auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathstring
Respostas
  • 204 Unblocked
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
NomeLocalTipoDescrição
limitquerystringItens por página (1–100).
cursorquerystringCursor da página anterior.
Respostas
  • 200 Feed page
  • 401 Token ausente, inválido, expirado ou revogado.
DELETE /v1/social/feed/{id} Delete your own post auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathstring
Respostas
  • 204 Deleted
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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
reactionobrigatórioclap · fire · strong · heart · wow
Respostas
  • 200 Reaction applied
  • 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.
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
  • 200 Friends
  • 401 Token ausente, inválido, expirado ou revogado.
Exemplo de resposta
{
    "data": [
        {
            "id": "usr_m8k…",
            "username": "joao.p",
            "display_name": "João P.",
            "friends_since": "2026-06-14T10:22:00Z",
            "shared": {
                "steps": 9840,
                "streak": 22
            }
        }
    ]
}
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_username returns the same 404 user_not_found as a user who does not exist. Two different responses would make the setting probeable.

Corpo · obrigatório
CampoTipoDescrição
usernameobrigatóriostring
sourceusername · qr · contacts · nearby · challenge
Respostas
  • 201 Request sent, or accepted if mutual
  • 404 Recurso inexistente.
  • 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/social/friends/requests Pending friend requests auth
Respostas
  • 200 Incoming and outgoing requests
  • 401 Token ausente, inválido, expirado ou revogado.
POST /v1/social/friends/requests/{id}/accept Accept a friend request auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathinteger
Respostas
  • 200 Accepted
  • 404 Recurso inexistente.
  • 409 Conflito com o estado atual.
POST /v1/social/friends/requests/{id}/decline Decline a friend request auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathinteger
Respostas
  • 200 Declined
  • 404 Recurso inexistente.
DELETE /v1/social/friends/{id} Remove a friend auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathstring
Respostas
  • 204 Removed
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
CampoTipoDescrição
user_idobrigatóriostring
contextobrigatórioprofile · nearby · challenge · feed · message
reasonobrigatórioharassment · spam · impersonation · inappropriate · underage · other
detailsstring?(max 1000)
Respostas
  • 201 Report filed
  • 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/social/users/{username} Public profile auth

A blocked user is indistinguishable from a non-existent one: both return 404.

Parâmetros
NomeLocalTipoDescrição
usernameobrigatóriopathstring
Respostas
  • 200 Profile
  • 404 Recurso inexistente.

Desafios

8 endpoints

Group challenges with live leaderboards.

GET /v1/challenges Your challenges auth
Parâmetros
NomeLocalTipoDescrição
statusqueryupcoming · 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
CampoTipoDescrição
titleobrigatóriostring(min 3, max 80)
descriptionstring?
metricobrigatóriosteps · water_ml · active_minutes · fasting_minutes · sleep_minutes · protein_g · mindful_minutes · daily_goal_hits · workouts
modetotal · average · streak · goal_hits
target_valuenumber?Required for goal_hits and streak modes.
visibilityprivate · friends · public
max_participantsinteger(min 2, max 200)
starts_onobrigatóriostring
ends_onobrigatóriostring
{
    "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
NomeLocalTipoDescrição
idobrigatóriopathstring
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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
usernamesobrigatórioarray<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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo
CampoTipoDescrição
join_codestring?
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
NomeLocalTipoDescrição
idobrigatóriopathstring
limitquerystringItens por página (1–100).
Respostas
  • 200 Leaderboard
  • 404 Recurso inexistente.
POST /v1/challenges/{id}/leave Leave a challenge auth
Parâmetros
NomeLocalTipoDescrição
idobrigatóriopathstring
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
NomeLocalTipoDescrição
idobrigatóriopathstring
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 endpoints

Weekly 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
NomeLocalTipoDescrição
limitquerystringItens por página (1–100).
Respostas
  • 200 Standings
  • 401 Token ausente, inválido, expirado ou revogado.

Nearby

3 endpoints

Opt-in discovery using approximate location only.

POST /v1/nearby/checkin Check in to an approximate area authconsent: nearby_location12/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
CampoTipoDescrição
latitudeobrigatórionumber(min -90, max 90)
longitudeobrigatórionumber(min -180, max 180)
activityobrigatóriowalk · run · gym · yoga · cycling · other
notestring?(max 140)
ttl_hoursinteger(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
NomeLocalTipoDescrição
activityquerywalk · 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 endpoints

Monthly 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
NomeLocalTipoDescrição
periodobrigatóriopathstring
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 endpoints

Subscriptions, 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
NomeLocalTipoDescrição
X-Idempotency-KeyheaderstringTorna a repetição segura.
Corpo · obrigatório
CampoTipoDescrição
original_transaction_idobrigatóriostring
product_idobrigatóriostring
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
CampoTipoDescrição
signedPayloadobrigatóriostringThe 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 endpoints

Signed 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

  1. Remova o objeto signature do corpo de GET /v1/config.
  2. Canonicalize o restante pela RFC 8785.
  3. Calcule SHA-256 desses bytes, em hexadecimal minúsculo.
  4. Verifique o JWS de signature.token com a chave cujo kid bate com o do cabeçalho.
  5. Confirme que a claim digest do JWS é igual ao hash calculado.
  6. Rejeite se exp já 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 endpoint

Product 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
CampoTipoDescrição
eventsobrigatórioarray<object>(máx itens 100)
session_idstring
device_idstring
app_versionstring
os_versionstring
{
    "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 endpoints

APNs 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
CampoTipoDescrição
rule_keyobrigatóriostring
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
CampoTipoDescrição
preferencesobrigatórioobject
{
    "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
CampoTipoDescrição
device_uuidobrigatóriostring
push_tokenobrigatóriostring
environmentsandbox · production
platformios · watchos · ipados
app_versionstring?
os_versionstring?
modelstring?
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
CampoTipoDescrição
device_uuidobrigatóriostring
Respostas
  • 204 Unregistered

Privacidade (LGPD)

7 endpoints

LGPD/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
CampoTipoDescrição
confirmobrigatóriotrue
reasonstring?
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
CampoTipoDescrição
formatjson · 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
NomeLocalTipoDescrição
idobrigatóriopathstring
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
NomeLocalTipoDescrição
idobrigatóriopathstring
tokenobrigatórioquerystring
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 endpoints

Pre-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
CampoTipoDescrição
emailobrigatóriostring
namestring?(max 80)
goallose_weight · gain_muscle · sleep_better · more_energy · general_health?
localestring?
websitestring?Honeypot. Must stay empty.
elapsedinteger?Seconds the form was on screen.
utm_sourcestring?
utm_mediumstring?
utm_campaignstring?
{
    "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 endpoints

Console 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
CampoTipoDescrição
emailobrigatóriostring
passwordobrigatóriostring
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
CampoTipoDescrição
codeobrigatóriostring
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
CampoTipoDescrição
codeobrigatóriostring(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
CampoTipoDescrição
current_passwordobrigatóriostring
new_passwordobrigatóriostring(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
NomeLocalTipoDescrição
qquerystring
planqueryfree · premium · lifetime
statusqueryactive · suspended · pending_deletion
limitquerystringItens 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
NomeLocalTipoDescrição
idobrigatóriopathstring
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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
planobrigatóriofree · premium · lifetime
daysinteger?
reasonobrigatóriostring(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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
noteobrigatóriostring(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
NomeLocalTipoDescrição
idobrigatóriopathstring
noteIdobrigatóriopathinteger
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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
statusobrigatórioactive · suspended
reasonstring?
Respostas
  • 200 Status alterado
  • 403 Autenticado, mas sem permissão (consentimento, opt-in, atestação).

Administração

13 endpoints

Internal 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
NomeLocalTipoDescrição
actionquerystring
limitquerystringItens 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
NomeLocalTipoDescrição
idobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
statusobrigatórioactive · rejected · merged
verifiedboolean
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
NomeLocalTipoDescrição
keyobrigatóriopathstring
Corpo · obrigatório
CampoTipoDescrição
valueobrigatórioanyAny JSON value.
typebool · int · float · string · json
descriptionstring?
enabledboolean
rollout_percentinteger(min 0, max 100)
is_publicbooleanFalse keeps the value server-side; it is never sent to the app.
min_app_versionstring?
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
NomeLocalTipoDescrição
keyobrigatóriopathstring
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
NomeLocalTipoDescrição
dayquerystring
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
NomeLocalTipoDescrição
fromquerystring
toquerystring
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
NomeLocalTipoDescrição
statusqueryopen · 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
NomeLocalTipoDescrição
idobrigatóriopathinteger
Corpo · obrigatório
CampoTipoDescrição
statusobrigatórioreviewing · actioned · dismissed
notestring?
actionnone · 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.

ComandoFrequênciaFaz o quê
ai:purge-photos15 minApaga fotos de prato vencidas, check-ins do Nearby e chaves expiradas. É o que torna a promessa de 24 h real.
privacy:process10 minMonta exports LGPD e executa exclusões de conta vencidas.
challenges:finalizehoráriaAtiva desafios que começaram e encerra os que terminaram.
push:dispatchhoráriaStreak em risco, fim de trial, inatividade.
billing:reconcilehoráriaExpira entitlements que a Apple nunca notificou.
maintenance:cleanupdiária 03:30Sessões, retenção de analytics, logs.
stats:percentilesdiária 03:45Recalcula pontos de corte populacionais do Wrapped.
cost:reportdiária 08:00Margem de contribuição. Sai com código ≠ 0 abaixo do piso, então o cron notifica.
leagues:rolloversegunda 00:05Fecha as ligas da semana, aplica promoções e rebaixamentos.
wrapped:generatedia 1º, 05:00Pré-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.

  1. Buscar /v1/config no cold start e verificar a assinatura ES256 antes de aplicar. Cache de 60 s.
  2. Sign in with Apple com nonce. Persistir o e-mail na primeira autorização — não virá de novo.
  3. Definir appAccountToken = user.public_id ao iniciar qualquer compra StoreKit. É o que permite atribuir a notificação da Apple a uma conta.
  4. Registrar o token APNs a cada launch — tokens rotacionam.
  5. Enrolar App Attest uma vez por instalação, antes da primeira análise de IA.
  6. Mandar X-Idempotency-Key em toda chamada de IA e em billing/link.
  7. Sincronizar agregados a cada hora (ou no interval_minutes da config), em lotes de até 120 dias.
  8. Ler /v1/me/quotas para mostrar “2 de 3 análises restantes” — não deixe o usuário descobrir o limite por uma requisição que falha.
  9. Mapear details.paywall do 402 para o paywall contextual correto.
  10. 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.