Skip to content

Laravel (PHP)

Laravel es el framework PHP más común en POS colombianos. Esta receta muestra el patrón completo: servicio HTTP, webhook con verificación HMAC y job en cola.

Emitia-Version: 2026-01-01


Terminal window
composer require guzzlehttp/guzzle
php artisan make:service EmitiaService
php artisan make:controller WebhookController
php artisan make:job ProcessEmitiaEvent

Variables de entorno en .env:

EMITIA_API_KEY=sk_test_TU_CLAVE
EMITIA_WEBHOOK_SECRET=whsec_...
EMITIA_BASE_URL=https://api.emitia.co/v1
EMITIA_VERSION=2026-01-01

app/Services/EmitiaService.php
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;
use Ramsey\Uuid\Uuid;
class EmitiaService
{
private string $baseUrl;
private string $apiKey;
private string $version;
public function __construct()
{
$this->baseUrl = config('services.emitia.base_url', 'https://api.emitia.co/v1');
$this->apiKey = config('services.emitia.api_key');
$this->version = config('services.emitia.version', '2026-01-01');
}
private function client(string $idempotencyKey = null): \Illuminate\Http\Client\PendingRequest
{
$headers = [
'Authorization' => "Bearer {$this->apiKey}",
'Emitia-Version' => $this->version,
];
if ($idempotencyKey) {
$headers['Idempotency-Key'] = $idempotencyKey;
}
return Http::baseUrl($this->baseUrl)->withHeaders($headers);
}
/** Verifica la autenticación (funciona hoy) */
public function getAccount(): array
{
return $this->client()->get('/account')->throw()->json();
}
/** Crea un cliente (disponible con api-resources) */
public function createCustomer(array $data): array
{
$key = Uuid::uuid4()->toString();
return $this->client($key)->post('/customers', $data)->throw()->json();
}
/** Crea una factura (disponible con api-resources + test-mode-sandbox) */
public function createInvoice(array $data, string $idempotencyKey): array
{
// El caller genera y persiste la clave ANTES de llamar
return $this->client($idempotencyKey)->post('/invoices', $data)->throw()->json();
}
}

app/Http/Controllers/InvoiceController.php
<?php
namespace App\Http\Controllers;
use App\Services\EmitiaService;
use Illuminate\Http\Request;
use Ramsey\Uuid\Uuid;
class InvoiceController extends Controller
{
public function store(Request $request, EmitiaService $emitia)
{
$validated = $request->validate([
'customer_nit' => 'required|string',
'customer_name' => 'required|string',
'description' => 'required|string',
'unit_price' => 'required|numeric|min:0',
'order_id' => 'required|string',
]);
// 1. Crear cliente
$customer = $emitia->createCustomer([
'organization_type' => '1',
'identification' => [
'type' => '31',
'number' => $validated['customer_nit'],
'check_digit' => '1',
],
'legal_name' => $validated['customer_name'],
'email' => 'facturacion@cliente.co',
]);
// 2. Generar y persistir la clave de idempotencia ANTES de llamar
$idempotencyKey = Uuid::uuid4()->toString();
// Guarda $idempotencyKey en tu DB asociado al pedido aquí
// 3. Crear factura
$unitPrice = number_format((float) $validated['unit_price'], 2, '.', '');
$taxValue = number_format((float) $validated['unit_price'] * 0.19, 2, '.', '');
$invoice = $emitia->createInvoice([
'customer_id' => $customer['id'],
'currency' => 'COP',
'operation_type' => '10',
'payment_means' => ['code' => '10', 'type' => '1'],
'lines' => [[
'description' => $validated['description'],
'quantity' => '1.00',
'unit_code' => '94',
'unit_price' => $unitPrice,
'line_extension_amount' => $unitPrice,
'taxes' => [[
'code' => '01',
'rate' => '19.00',
'base' => $unitPrice,
'value' => $taxValue,
]],
]],
'metadata' => ['order_id' => $validated['order_id']],
], $idempotencyKey);
return response()->json($invoice, 202);
}
}

app/Http/Controllers/WebhookController.php
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessEmitiaEvent;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class WebhookController extends Controller
{
public function handle(Request $request): Response
{
$rawBody = $request->getContent();
$signature = $request->header('Emitia-Signature', '');
$secret = config('services.emitia.webhook_secret');
if (!$this->verifySignature($rawBody, $signature, $secret)) {
return response('Firma inválida', 401);
}
$event = json_decode($rawBody, true);
// Despacha el job en cola — responde rápido al webhook
ProcessEmitiaEvent::dispatch($event);
return response('', 200);
}
private function verifySignature(string $rawBody, string $signature, string $secret): bool
{
parse_str(str_replace(',', '&', $signature), $parts);
$t = isset($parts['t']) ? (int) $parts['t'] : null;
$v1 = $parts['v1'] ?? null;
if ($t === null || $v1 === null) {
return false;
}
// Verificar timestamp (anti-replay, tolerancia 5 min)
if (abs(time() - $t) > 300) {
return false;
}
$expected = hash_hmac('sha256', "{$t}.{$rawBody}", $secret);
return hash_equals($expected, $v1);
}
}

app/Jobs/ProcessEmitiaEvent.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProcessEmitiaEvent implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(private array $event) {}
public function handle(): void
{
match ($this->event['type']) {
'invoice.accepted_by_dian' => $this->handleAccepted(),
'invoice.rejected_by_dian' => $this->handleRejected(),
default => Log::info('Emitia event ignored: ' . $this->event['type']),
};
}
private function handleAccepted(): void
{
$invoiceId = $this->event['data']['object']['id'];
$cufe = $this->event['data']['object']['cufe'] ?? '';
Log::info("Factura aceptada: {$invoiceId} CUFE: {$cufe}");
// Actualizar estado en DB, notificar al usuario, etc.
}
private function handleRejected(): void
{
$invoiceId = $this->event['data']['object']['id'];
Log::error("Factura rechazada: {$invoiceId}");
// Notificar al equipo, registrar error, etc.
}
}

routes/api.php
Route::post('/webhooks/emitia', [WebhookController::class, 'handle']);