π
ΒΏ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
πΊοΈ
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
*/
function generateJwtAssertion(
string $privateKey,
string $privateKeyId,
string $clientEmail,
string $tokenUri = 'https://authn.masstack.com/v1/oauth/token'
): string {
// Header del JWT
$header = base64UrlEncode(json_encode([
'alg' => 'RS256',
'typ' => 'JWT',
'kid' => $privateKeyId,
]));
// Payload del JWT β vΓ‘lido 1 hora
$now = time();
$payload = base64UrlEncode(json_encode([
'iss' => $clientEmail,
'aud' => $tokenUri,
'iat' => $now,
'exp' => $now + 3600,
]));
// Firmar con la clave privada usando SHA-256 (RS256)
$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(
string $privateKey,
string $privateKeyId,
string $clientEmail
): array {
$tokenUri = 'https://authn.masstack.com/v1/oauth/token';
$assertion = generateJwtAssertion($privateKey, $privateKeyId, $clientEmail, $tokenUri);
$ch = curl_init($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);
$response = json_decode($body, true);
if ($status !== 200) {
throw new RuntimeException("Error obteniendo token ($status): $body");
}
return $response;
// Devuelve: ['access_token' => '...', 'expires_in' => 3600, 'token_type' => 'Bearer']
}
// --- Uso ---
$config = json_decode(file_get_contents('/ruta/service_account.json'), true);
$tokenData = getMasStackToken(
$config['private_key'],
$config['private_key_id'],
$config['client_email']
);
$accessToken = $tokenData['access_token'];
// Guarda $accessToken y $tokenData['expires_in'] para reutilizarlo durante 1 hora
<?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,
array $address,
string $sfChannel = '',
string $sfSegment = ''
): array {
$url = "https://feasibility.masstack.com/v1/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 ---
$buildings = searchBuilding($accessToken, $orgId, [
'way_type' => 'Calle',
'way_name' => 'Mayor',
'number' => '1',
'town' => 'Madrid',
'postal_code' => '28001',
], $sfChannel, $sfSegment);
// Coge el primer resultado
$building = $buildings[0] ?? null;
$technicalId = $building['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
*
* El parΓ‘metro unit_to_data=true incluye los datos de cada unidad (piso/local)
* del edificio, con sus tecnologΓas disponibles por unidad.
*/
function getBuildingCoverage(
string $accessToken,
string $orgId,
string $technicalId,
string $sfChannel = '',
string $sfSegment = ''
): array {
$url = "https://feasibility.masstack.com/v1/orgs/$orgId/coverage/buildings/$technicalId?unit_to_data=true";
$checkId = generateUuid(); // reutiliza la funciΓ³n del paso 2
$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, $orgId, $technicalId, $sfChannel, $sfSegment);
// Interpretar resultado
$summary = $coverage['_building_summary'] ?? $coverage;
$territoryOwners = $summary['territory_owners'] ?? [];
$technologies = $summary['technologies'] ?? [];
echo "Edificio: " . ($summary['way_name'] ?? '') . " " . ($summary['number'] ?? '') . "\n";
echo "TecnologΓas disponibles: " . implode(', ', $technologies) . "\n";
echo "Operadores con cobertura:\n";
foreach ($territoryOwners as $op) {
echo " - " . ($op['name'] ?? $op) . "\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 = 'https://authn.masstack.com/v1/oauth/token';
private string $apiBase = 'https://feasibility.masstack.com/v1';
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'] ?? '';
}
/** Carga la configuraciΓ³n desde el JSON de service account */
public static function fromServiceAccount(string $jsonPath, string $orgId, string $sfChannel = '', string $sfSegment = ''): 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'],
'org_id' => $orgId,
'sf_channel' => $sfChannel,
'sf_segment' => $sfSegment,
]);
}
/** Comprueba cobertura FTTH en una direcciΓ³n - mΓ©todo principal */
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']
);
}
// ββ Privados ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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',
'TU_ORG_ID',
'TU_SF_CHANNEL',
'TU_SF_SEGMENT'
);
$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";
}
βΉοΈ
Credenciales necesarias: AdemΓ‘s de la clave RSA privada, necesitas
private_key_id, client_email, org_id, y opcionalmente sf_channel / sf_segment. Todos estos valores estΓ‘n en el fichero service_account.json que proporciona MasStack al dar de alta la cuenta.