'Method not allowed']); exit; } $rawBody = file_get_contents('php://input'); if (!$rawBody) { http_response_code(400); echo json_encode(['error' => 'Empty body']); exit; } $payload = json_decode($rawBody, true); if ($payload === null) { http_response_code(400); echo json_encode(['error' => 'Invalid JSON']); exit; } // Verify HMAC signature if secret is configured if (BTCPAY_WEBHOOK_SECRET) { $sigHeader = $_SERVER['HTTP_BTCPAY_SIG'] ?? ''; $expectedSig = 'sha256=' . hash_hmac('sha256', $rawBody, BTCPAY_WEBHOOK_SECRET); if (!hash_equals($expectedSig, $sigHeader)) { http_response_code(403); echo json_encode(['error' => 'Invalid signature']); exit; } } $type = $payload['type'] ?? null; if (!$type) { http_response_code(400); echo json_encode(['error' => 'Missing type']); exit; } // Only handle fully settled invoices (not partial payment events) if ($type !== 'InvoiceSettled') { echo json_encode(['ok' => true, 'action' => 'ignored', 'type' => $type]); exit; } // Read listingId directly from webhook payload metadata $listingId = $payload['metadata']['listingId'] ?? null; if (!$listingId) { echo json_encode(['ok' => true, 'action' => 'skipped', 'reason' => 'No listingId in metadata']); exit; } // Update listing in Directus: publish + set paid $expiresAt = date('c', strtotime('+30 days')); $now = date('c'); $directusPayload = json_encode([ 'status' => 'published', 'payment_status' => 'paid', 'paid_at' => $now, 'expires_at' => $expiresAt, ]); $directusUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId); $ch = curl_init($directusUrl); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => 'PATCH', CURLOPT_POSTFIELDS => $directusPayload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . DIRECTUS_TOKEN, ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, ]); $directusResponse = curl_exec($ch); $directusStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($directusResponse === false || $curlError) { http_response_code(502); echo json_encode([ 'error' => 'Connection to Directus failed', 'listingId' => $listingId, 'curl_error' => $curlError, ]); exit; } if ($directusStatus >= 400) { http_response_code(502); echo json_encode([ 'error' => 'Directus returned error', 'status' => $directusStatus, 'listingId' => $listingId, 'response' => substr($directusResponse, 0, 500), ]); exit; } // Fetch listing to get owner $getUrl = DIRECTUS_URL . '/items/listings/' . urlencode($listingId) . '?fields=user_created'; $gch = curl_init($getUrl); curl_setopt_array($gch, [ CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . DIRECTUS_TOKEN, ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, ]); $getResponse = curl_exec($gch); curl_close($gch); $listingData = json_decode($getResponse, true); $userCreated = $listingData['data']['user_created'] ?? null; // Create notification for listing owner (skip if already exists) if ($userCreated) { $checkUrl = DIRECTUS_URL . '/items/notifications?' . http_build_query([ 'filter' => json_encode([ 'user_hash' => ['_eq' => $userCreated], 'type' => ['_eq' => 'listing_published'], 'reference_id' => ['_eq' => $listingId], ]), 'limit' => 1, ]); $ech = curl_init($checkUrl); curl_setopt_array($ech, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . DIRECTUS_TOKEN], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, ]); $existingResp = curl_exec($ech); curl_close($ech); $existingData = json_decode($existingResp, true); if (empty($existingData['data'])) { $notifPayload = json_encode([ 'user_hash' => $userCreated, 'type' => 'listing_published', 'reference_id' => $listingId, 'read' => false, ]); $notifUrl = DIRECTUS_URL . '/items/notifications'; $nch = curl_init($notifUrl); curl_setopt_array($nch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $notifPayload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . DIRECTUS_TOKEN, ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, ]); curl_exec($nch); curl_close($nch); } } echo json_encode(['ok' => true, 'listingId' => $listingId, 'action' => 'published']);