API do curso · versão 1
API Tempo Açores
Uma API REST mantida pelo docente: as ilhas e os trilhos dos Açores, a previsão real do IPMA, e um feed de relatos da turma com texto e fotografia. É a base do mini-projeto 2.
| Endereço base | https://api-pdm.sublimecipher.com |
|---|---|
| Documentação interativa | https://api-pdm.sublimecipher.com/docs (Swagger: experimentas cada pedido no browser) |
| Especificação OpenAPI | https://api-pdm.sublimecipher.com/openapi.json |
| Formato | JSON em UTF-8; datas em ISO 8601; números como números |
| Autenticação | JWT: Authorization: Bearer <token> em todos os pedidos menos o do token |
Primeiros passos
- O teu
clientIde o teuclientSecretestão numa issue no teu repositóriopdm-miniprojeto. São só teus. - Abre a documentação interativa. Em POST /v1/auth/token, carrega em Try it out, preenche o pedido e carrega em Execute.
- Copia o
accessToken, carrega em Authorize no topo da página e cola-o. A partir daí todos os pedidos levam o token. - Experimenta GET /v1/islands/sao-miguel/forecast: é a previsão de hoje, vinda do IPMA.
Autenticação
Trocas o clientId e o clientSecret por um token, e mandas o token em cada pedido. O token dura 1 hora; quando a API responder 401, pede outro.
POST /v1/auth/token
Content-Type: application/json
{
"clientId": "pdm-ab12cd",
"clientSecret": "o-teu-segredo"
}
{
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"tokenType": "Bearer",
"expiresIn": 3600
}
GET /v1/islands
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
clientSecret, esse sim, não pode ir para o código que fazes commit: guarda-o fora do git, por exemplo com --dart-define.Endpoints
| Método e caminho | O que faz |
|---|---|
POST /v1/auth/token | Troca o clientId e o clientSecret por um token (sem token). |
GET /v1/me | Quem és, segundo o token. Bom para testar. |
GET /v1/islands | As nove ilhas. |
GET /v1/islands/{id} | Uma ilha. |
GET /v1/islands/{id}/forecast | Previsão a 5 dias da ilha (IPMA). |
GET /v1/trails | Os trilhos; ?islandId=faial filtra por ilha. |
GET /v1/trails/{id} | Um trilho. |
GET /v1/reports | Feed de relatos da turma, mais recentes primeiro; ?islandId= e ?limit= (até 100). |
POST /v1/reports | Cria um relato: só texto (JSON) ou texto e fotografia (multipart). |
GET /v1/reports/{id} | Um relato. |
PUT /v1/reports/{id} | Altera o texto ou a ilha de um relato teu. |
PUT /v1/reports/{id}/photo | Põe ou substitui a fotografia de um relato teu (multipart). |
DELETE /v1/reports/{id} | Apaga um relato teu. |
Ilhas e trilhos
{
"id": "sao-miguel",
"name": "São Miguel",
"imageUrl": "https://api-pdm.sublimecipher.com/static/islands/sao_miguel.jpg",
"trailCount": 3
}
{
"id": "caldeira-faial",
"name": "Volta à Caldeira do Faial",
"islandId": "faial",
"islandName": "Faial",
"imageUrl": "https://api-pdm.sublimecipher.com/static/islands/faial.jpg",
"distanceKm": 7.0,
"duration": "2 h 30",
"difficulty": "easy",
"difficultyLabel": "Fácil",
"description": "À volta da cratera com 2 km de diâmetro no centro da ilha."
}
Os ids são os mesmos da app das aulas: sao-miguel, santa-maria, terceira, graciosa, sao-jorge, pico, faial, flores, corvo. As fotografias das ilhas estão em imageUrl: usa Image.network em vez de Image.asset.
Previsão
{
"islandId": "sao-miguel",
"islandName": "São Miguel",
"location": "Ponta Delgada",
"source": "IPMA",
"updatedAt": "2026-10-13T09:31:02",
"days": [
{
"date": "2026-10-13",
"weatherType": {
"id": 2,
"description": "Céu pouco nublado",
"category": "partlyCloudy"
},
"tMin": 17.6,
"tMax": 23.6,
"rainChance": 0,
"wind": {
"direction": "SE",
"class": 2,
"description": "Moderado"
}
},
"..."
]
}
weatherType.categoryé um dos valores do enumWeatherTypedas aulas:sunny,partlyCloudy,cloudy,rainoustorm. Adescriptioné o texto do IPMA.rainChancevai de 0 a 100. O IPMA não dá a velocidade do vento em km/h, só a classe: 1 fraco, 2 moderado, 3 forte, 4 muito forte.- A API pergunta ao IPMA no máximo de 30 em 30 minutos. Se o IPMA falhar, devolve a última previsão que tiver; se não tiver nenhuma, responde
502.
Relatos
Um relato é um texto, e opcionalmente uma fotografia, sobre o tempo numa ilha. O feed é da turma toda: todos veem os relatos de todos, com o nome do autor, e cada um só altera e apaga os seus. O campo mine diz se o relato é teu.
{
"id": "e7e97972-2db3-47d0-a817-6912481e64ad",
"islandId": "faial",
"islandName": "Faial",
"text": "Céu limpo na Horta, mar calmo.",
"photoUrl": "https://api-pdm.sublimecipher.com/media/be622df7-cfa6-45fb-bd71-c6cd92a763cf.jpg",
"author": {
"clientId": "pdm-ab12cd",
"name": "Maria Silva"
},
"mine": true,
"createdAt": "2026-10-13T09:59:43.115515Z",
"updatedAt": "2026-10-13T09:59:43.115515Z"
}
POST /v1/reports
Authorization: Bearer ...
Content-Type: application/json
{
"islandId": "faial",
"text": "Céu limpo na Horta, mar calmo."
}
POST /v1/reports
Authorization: Bearer ...
Content-Type: multipart/form-data
islandId = faial
text = Céu limpo na Horta, mar calmo.
photo = (ficheiro JPEG, PNG ou WebP)
PUT /v1/reports/e7e97972-2db3-47d0-a817-6912481e64ad
Authorization: Bearer ...
Content-Type: application/json
{
"text": "Afinal começou a chover às 17h."
}
POSTresponde201com o relato criado;PUTresponde200com o relato alterado;DELETEresponde204, sem corpo.- O texto tem de 1 a 500 caracteres. A fotografia tem até 8 MB; a API reduz para 1280 px no lado maior e grava em JPEG.
- Alterar ou apagar um relato que não é teu dá
403.
Erros
Quando algo corre mal, o código HTTP diz o tipo de erro e o corpo explica-o em português:
{
"error": {
"code": "not_found",
"message": "Não há nenhuma ilha com o id \"atlantida\"."
}
}
| Código | Quando | O que a app deve fazer |
|---|---|---|
400 | Pedido inválido: campo em falta, texto vazio ou longo, fotografia que não é imagem | Mostrar a message ao utilizador |
401 | Sem token, token inválido ou expirado | Pedir um token novo e repetir o pedido |
403 | O relato não é teu | Não mostrar editar e apagar nos relatos dos outros (mine) |
404 | A ilha, o trilho ou o relato não existe | Mostrar "não encontrado" |
413 | Fotografia com mais de 8 MB | Reduzir a fotografia antes de enviar |
502 | O IPMA não respondeu | Mostrar um erro com "Tentar outra vez" |
Em Dart
Com o pacote http (flutter pub add http). Os exemplos foram testados contra a API.
import 'dart:convert';
import 'package:http/http.dart' as http;
const base = 'https://api-pdm.sublimecipher.com';
Future<String> getToken(String clientId, String clientSecret) async {
final res = await http.post(
Uri.parse('$base/v1/auth/token'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'clientId': clientId, 'clientSecret': clientSecret}),
);
if (res.statusCode != 200) {
throw Exception(jsonDecode(res.body)['error']['message']);
}
return jsonDecode(res.body)['accessToken'];
}
final res = await http.get(
Uri.parse('$base/v1/islands/faial/forecast'),
headers: {'Authorization': 'Bearer $token'},
);
final days = jsonDecode(res.body)['days'] as List<dynamic>;
final req = http.MultipartRequest('POST', Uri.parse('$base/v1/reports'))
..headers['Authorization'] = 'Bearer $token'
..fields['islandId'] = 'faial'
..fields['text'] = 'Céu limpo na Horta, mar calmo.'
..files.add(await http.MultipartFile.fromPath('photo', file.path));
final res = await http.Response.fromStream(await req.send());
// 201: jsonDecode(res.body) é o relato criado, com photoUrl
final res = await http.put(
Uri.parse('$base/v1/reports/$id'),
headers: {'Authorization': 'Bearer $token', 'Content-Type': 'application/json'},
body: jsonEncode({'text': 'Afinal começou a chover às 17h.'}),
);
No Chrome, MultipartFile.fromPath não funciona (não há sistema de ficheiros): usa MultipartFile.fromBytes('photo', bytes, filename: 'foto.jpg') com os bytes que o seletor de imagens devolve.
Regras de uso
- As credenciais são pessoais. Os pedidos ficam registados com o teu nome, e os relatos que publicares aparecem com ele à turma toda.
- Os relatos são públicos dentro da turma: escreve como escreverias num grupo da turma. O docente pode apagar qualquer relato.
- Faz cache do lado da app: a previsão só muda de 30 em 30 minutos, não precisas de a pedir a cada ecrã.
- Se perderes o
clientSecret, pede ao docente um novo.