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
Configuración inicial
Section titled “Configuración inicial”composer require guzzlehttp/guzzlephp artisan make:service EmitiaServicephp artisan make:controller WebhookControllerphp artisan make:job ProcessEmitiaEventVariables de entorno en .env:
EMITIA_API_KEY=sk_test_TU_CLAVEEMITIA_WEBHOOK_SECRET=whsec_...EMITIA_BASE_URL=https://api.emitia.co/v1EMITIA_VERSION=2026-01-01Servicio Emitia
Section titled “Servicio Emitia”<?phpnamespace 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(); }}Crear cliente y factura
Section titled “Crear cliente y factura”<?phpnamespace 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); }}Webhook Controller
Section titled “Webhook Controller”<?phpnamespace 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); }}Job de procesamiento
Section titled “Job de procesamiento”<?phpnamespace 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. }}Ruta del webhook
Section titled “Ruta del webhook”Route::post('/webhooks/emitia', [WebhookController::class, 'handle']);Qué sigue
Section titled “Qué sigue”- Conceptos: Webhooks — detalles de la verificación HMAC
- Conceptos: Idempotencia — el patrón de retry seguro
- Guía de mapeo POS — códigos DIAN para tu POS