π
ΒΏCΓ³mo funciona?
El sistema consulta la API MasFeasibility v1 de MasStack para determinar si una direcciΓ³n postal tiene cobertura de fibra Γ³ptica NEBA/FTTH disponible. El proceso siempre sigue estos 3 pasos:
π
1. AutenticaciΓ³n JWT
Firma JWT con clave RSA privada y obtiene token OAuth 2.0
π’
2. Buscar edificio
Localiza el edificio en el catΓ‘logo por direcciΓ³n postal
π‘
3. Consultar cobertura
Obtiene datos detallados de cobertura para ese edificio
β
4. Resultado
TecnologΓas disponibles, operadores y velocidades
ProducciΓ³n Β· feasibility.masstack.com
πΊοΈ
Consultar cobertura por direcciΓ³n
π
Resultado de cobertura
π¬
Detalle de llamadas a la API
1
AutenticaciΓ³n OAuth 2.0
Genera un JWT firmado con RS256 y lo intercambia por un access token
2
BΓΊsqueda de edificio
POST /orgs/{orgId}/coverage/buildings/search
3
Datos de cobertura del edificio
GET /orgs/{orgId}/coverage/buildings/{buildingId}?unit_to_data=true
π
CΓ³mo implementarlo en PHP
<?php
/**
* MasStack - Paso 1: AutenticaciΓ³n OAuth 2.0 con JWT Bearer
*
* La API usa el flujo "JWT Bearer Grant" del estΓ‘ndar OAuth 2.0.
* Necesitas firmar un JWT con tu clave RSA privada (RS256) y enviarlo
* al endpoint de tokens para recibir un access token Bearer.
*
* Credenciales necesarias (del fichero service_account.json):
* - private_key β tu clave RSA privada
* - private_key_id β el identificador de la clave (kid)
* - client_email β ej: thirdparty@XXXX.auth.masmovil.com
* - token_uri β https://authn.masstack.com/v1/oauth/token (PROD)
* β https://authn.sta.masstack.com/v1/oauth/token (STA)
*/
function generateJwtAssertion(
string $privateKey,
string $privateKeyId,
string $clientEmail,
string $tokenUri
): string {
$header = base64UrlEncode(json_encode([
'alg' => 'RS256',
'typ' => 'JWT',
'kid' => $privateKeyId,
]));
$now = time();
$payload = base64UrlEncode(json_encode([
'iss' => $clientEmail,
'aud' => $tokenUri,
'iat' => $now,
'exp' => $now + 3600,
]));
$data = "$header.$payload";
$keyRes = openssl_pkey_get_private($privateKey);
openssl_sign($data, $signature, $keyRes, OPENSSL_ALGO_SHA256);
return "$data." . base64UrlEncode($signature);
}
function base64UrlEncode(string $data): string {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function getMasStackToken(array $config): array {
$assertion = generateJwtAssertion(
$config['private_key'],
$config['private_key_id'],
$config['client_email'],
$config['token_uri']
);
$ch = curl_init($config['token_uri']);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_POSTFIELDS => http_build_query([
'assertion' => $assertion,
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Error obteniendo token ($status): $body");
}
return json_decode($body, true);
// Devuelve: ['access_token' => '...', 'expires_in' => 3600, 'token_type' => 'Bearer']
}
// --- Uso (PROD) ---
$config = [
'private_key' => file_get_contents('/ruta/private_key.pem'),
'private_key_id' => '9fe70aff-...',
'client_email' => 'lemonvil-sa@XXXX.auth.masmovil.com',
'token_uri' => 'https://authn.masstack.com/v1/oauth/token',
];
$tokenData = getMasStackToken($config);
$accessToken = $tokenData['access_token'];
<?php
/**
* MasStack - Paso 2: Buscar edificio por direcciΓ³n postal
*
* Endpoint: POST /orgs/{orgId}/coverage/buildings/search
*
* Cabeceras requeridas:
* - Authorization: Bearer {token}
* - sf-check-id: UUID ΓΊnico por peticiΓ³n (para trazabilidad)
* - sf-channel: cΓ³digo de canal (te lo proporciona MasStack)
* - sf-segment: cΓ³digo de segmento (te lo proporciona MasStack)
*/
function searchBuilding(
string $accessToken,
string $orgId,
string $apiBase,
array $address,
string $sfChannel = '',
string $sfSegment = ''
): array {
$url = "$apiBase/orgs/$orgId/coverage/buildings/search";
$checkId = generateUuid();
$headers = [
"Authorization: Bearer $accessToken",
'Content-Type: application/json',
"sf-check-id: $checkId",
];
if ($sfChannel) $headers[] = "sf-channel: $sfChannel";
if ($sfSegment) $headers[] = "sf-segment: $sfSegment";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($address),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Error buscando edificio ($status): $body");
}
return json_decode($body, true);
// Devuelve un array de edificios con su 'technical_id'
}
function generateUuid(): string {
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
// --- Uso (PROD) ---
$apiBase = 'https://feasibility.masstack.com/v1'; // PROD
// $apiBase = 'https://feasibility.sta.masstack.com/v1'; // STA
$buildings = searchBuilding($accessToken, 'MARBLANCA', $apiBase, [
'way_type' => 'Calle',
'way_name' => 'Mayor',
'number' => '1',
'town' => 'Madrid',
'postal_code' => '28001',
], 'LEMONVIL', 'RES');
$technicalId = $buildings[0]['technical_id'] ?? null;
if (!$technicalId) {
echo "No se encontrΓ³ ningΓΊn edificio para esa direcciΓ³n.";
exit;
}
<?php
/**
* MasStack - Paso 3: Obtener cobertura del edificio
*
* Endpoint: GET /orgs/{orgId}/coverage/buildings/{technicalId}?unit_to_data=true
*/
function getBuildingCoverage(
string $accessToken,
string $orgId,
string $apiBase,
string $technicalId,
string $sfChannel = '',
string $sfSegment = ''
): array {
$url = "$apiBase/orgs/$orgId/coverage/buildings/$technicalId?unit_to_data=true";
$checkId = generateUuid();
$headers = [
"Authorization: Bearer $accessToken",
"sf-check-id: $checkId",
];
if ($sfChannel) $headers[] = "sf-channel: $sfChannel";
if ($sfSegment) $headers[] = "sf-segment: $sfSegment";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException("Error obteniendo cobertura ($status): $body");
}
return json_decode($body, true);
}
// --- Uso ---
$coverage = getBuildingCoverage($accessToken, 'MARBLANCA', $apiBase, $technicalId, 'LEMONVIL', 'RES');
$summary = $coverage['_building_summary'] ?? $coverage;
$technologies = $summary['technologies'] ?? [];
$operators = $summary['territory_owners'] ?? [];
echo "TecnologΓas disponibles: " . implode(', ', $technologies) . "\n";
$hasFTTH = in_array('FTTH', $technologies, true);
echo $hasFTTH ? "β
HAY COBERTURA FTTH/NEBA\n" : "β NO hay cobertura FTTH en esta direcciΓ³n\n";
<?php
class MasStackClient
{
private string $privateKey;
private string $privateKeyId;
private string $clientEmail;
private string $orgId;
private string $sfChannel;
private string $sfSegment;
private string $tokenUri;
private string $apiBase;
private ?string $token = null;
private int $tokenExp = 0;
public function __construct(array $config)
{
$this->privateKey = $config['private_key'];
$this->privateKeyId = $config['private_key_id'];
$this->clientEmail = $config['client_email'];
$this->orgId = $config['org_id'];
$this->sfChannel = $config['sf_channel'] ?? '';
$this->sfSegment = $config['sf_segment'] ?? '';
$this->tokenUri = $config['token_uri'];
$this->apiBase = $config['api_base'];
}
/** Carga la configuraciΓ³n desde el JSON de service account */
public static function fromServiceAccount(
string $jsonPath,
string $orgId,
string $sfChannel = '',
string $sfSegment = '',
string $env = 'prod' // 'prod' o 'sta'
): self {
$data = json_decode(file_get_contents($jsonPath), true);
return new self([
'private_key' => $data['private_key'],
'private_key_id' => $data['private_key_id'],
'client_email' => $data['client_email'],
'token_uri' => $data['token_uri'],
'org_id' => $orgId,
'sf_channel' => $sfChannel,
'sf_segment' => $sfSegment,
'api_base' => $env === 'sta'
? 'https://feasibility.sta.masstack.com/v1'
: 'https://feasibility.masstack.com/v1',
]);
}
public function checkCoverage(array $address): array
{
$buildings = $this->searchBuildings($address);
if (empty($buildings)) {
return ['found' => false, 'message' => 'DirecciΓ³n no encontrada en el catΓ‘logo'];
}
$building = $buildings[0];
$coverage = $this->getBuildingCoverage($building['technical_id']);
$summary = $coverage['_building_summary'] ?? $coverage;
$technologies = $summary['technologies'] ?? [];
$operators = $summary['territory_owners'] ?? [];
return [
'found' => true,
'ftth' => in_array('FTTH', $technologies, true),
'technologies' => $technologies,
'operators' => $operators,
'building' => $building,
'raw' => $coverage,
];
}
public function searchBuildings(array $address): array
{
return $this->request('POST', "/orgs/{$this->orgId}/coverage/buildings/search", $address);
}
public function getBuildingCoverage(string $technicalId): array
{
return $this->request('GET', "/orgs/{$this->orgId}/coverage/buildings/$technicalId", null, ['unit_to_data' => 'true']);
}
private function getToken(): string
{
if ($this->token && time() < $this->tokenExp - 60) return $this->token;
$assertion = $this->buildJwt();
$ch = curl_init($this->tokenUri);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_POSTFIELDS => http_build_query([
'assertion' => $assertion,
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) throw new RuntimeException("Auth error $status: $body");
$data = json_decode($body, true);
$this->token = $data['access_token'];
$this->tokenExp = time() + $data['expires_in'];
return $this->token;
}
private function buildJwt(): string
{
$now = time();
$head = $this->b64u(json_encode(['alg' => 'RS256', 'typ' => 'JWT', 'kid' => $this->privateKeyId]));
$body = $this->b64u(json_encode(['iss' => $this->clientEmail, 'aud' => $this->tokenUri, 'iat' => $now, 'exp' => $now + 3600]));
$data = "$head.$body";
openssl_sign($data, $sig, openssl_pkey_get_private($this->privateKey), OPENSSL_ALGO_SHA256);
return "$data." . $this->b64u($sig);
}
private function b64u(string $d): string { return rtrim(strtr(base64_encode($d), '+/', '-_'), '='); }
private function request(string $method, string $path, ?array $body = null, array $query = []): array
{
$token = $this->getToken();
$url = $this->apiBase . $path . ($query ? '?' . http_build_query($query) : '');
$headers = [
"Authorization: Bearer $token",
'Content-Type: application/json',
'sf-check-id: ' . $this->uuid(),
];
if ($this->sfChannel) $headers[] = "sf-channel: {$this->sfChannel}";
if ($this->sfSegment) $headers[] = "sf-segment: {$this->sfSegment}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
]);
$resp = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) throw new RuntimeException("API error $status on $method $path: $resp");
return json_decode($resp, true);
}
private function uuid(): string
{
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff),
mt_rand(0,0x0fff)|0x4000, mt_rand(0,0x3fff)|0x8000,
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
);
}
}
// ββ Ejemplo de uso βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
$client = MasStackClient::fromServiceAccount(
'/ruta/service_account.json',
'MARBLANCA',
'LEMONVIL',
'RES',
'prod' // cambiar a 'sta' para staging
);
$result = $client->checkCoverage([
'way_type' => 'Calle',
'way_name' => 'Mayor',
'number' => '1',
'town' => 'Madrid',
'postal_code' => '28001',
]);
if ($result['ftth']) {
echo "β
HAY cobertura FTTH/NEBA\n";
echo "TecnologΓas: " . implode(', ', $result['technologies']) . "\n";
} else {
echo "β NO hay cobertura FTTH en esta direcciΓ³n\n";
}
βΉοΈ
Dos entornos disponibles:
PROD usa
authn.masstack.com + feasibility.masstack.com Β·
STA usa authn.sta.masstack.com + feasibility.sta.masstack.com.
Cada entorno tiene su propio service account con claves RSA distintas.