Service Class
// app/Services/PaychainlyService.php
class PaychainlyService {
public function createPaymentLink(float $amount, string $description, array $metadata = []): array {
$response = Http::withHeaders(['x-api-key' => config('services.paychainly.key')])
->post('https://api.paychainly.com/api/v1/payment-links', compact('amount', 'description', 'metadata'));
return $response->json('data');
}
public function verifySignature(string $payload, string $signature): bool {
$expected = 'sha256=' . hash_hmac('sha256', $payload, config('services.paychainly.webhook_secret'));
return hash_equals($expected, $signature);
}
}
Webhook Controller
class PaychainlyWebhookController extends Controller {
public function handle(Request $request, PaychainlyService $paychainly) {
$payload = $request->getContent();
$sig = $request->header('X-Paychainly-Signature');
abort_unless($paychainly->verifySignature($payload, $sig), 401);
$event = $request->json()->all();
ProcessPaymentJob::dispatchIf(
!ProcessedPayment::where('tx_hash', $event['txHash'])->exists(),
$event
);
return response()->json(['status' => 'ok']);
}
}
Queue Job
class ProcessPaymentJob implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle() {
ProcessedPayment::firstOrCreate(
['tx_hash' => $this->event['txHash']],
['user_id' => $this->event['userId'], 'amount' => $this->event['amount']]
);
// Credit user, send email, etc.
}
}
Config
// config/services.php
'paychainly' => [
'key' => env('PAYCHAINLY_API_KEY'),
'webhook_secret' => env('PAYCHAINLY_WEBHOOK_SECRET'),
],
Route
Route::post('/webhooks/paychainly', [PaychainlyWebhookController::class, 'handle'])
->withoutMiddleware(['auth', 'throttle']); // bypass auth but keep signature check