callstorm / sdk
Official PHP SDK for the Callstorm external API (outbound voice campaigns).
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
SDK oficial para integrar la API externa de Callstorm (campañas de voz salientes: audio, voicebot y agente IA)
desde cualquier aplicación PHP 8.1+. Cubre todas las rutas de https://api.callstorm.ai/v1/ext, la paginación por
cursor, la idempotencia, el límite de tasa, el mapeo de errores a excepciones y la verificación de webhooks.
- Sin dependencias en tiempo de ejecución más allá de
ext-curlyext-json. - Namespace PSR-4
Callstorm\, tipado estricto, compatible con PHP 8.1, 8.2 y 8.3. - Transporte HTTP intercambiable (interfaz
Callstorm\Http\Transport) para usar Guzzle, PSR-18 o un fake en tests.
Referencia de la API: callstorm.ai/developers y el documento OpenAPI
https://api.callstorm.ai/v1/public/openapi.json.
Índice
- Instalación
- Inicio rápido
- Autenticación y scopes
- Recursos
- Paginación
- Idempotencia
- Límite de tasa
- Errores
- Subida de listas de contactos
- Webhooks
- Consola de agente en tu CRM (widget)
- Buenas prácticas
- Versionado y compatibilidad
Instalación
Requisitos: PHP ≥ 8.1 con ext-curl y ext-json (ambas vienen activadas en casi todas las distribuciones).
composer require callstorm/sdk
El paquete está en Packagist; el código fuente, en github.com/dvalenci/callstorm-sdk-php.
Inicio rápido
<?php require 'vendor/autoload.php'; use Callstorm\Client; $client = new Client(getenv('VOXCORE_API_KEY')); // vx_live_… en producción, vx_test_… en dev // 1. Subir el archivo de contactos y crear la lista (cada columna salvo el teléfono viaja como variable). $list = $client->leadLists()->createFromFile('deudores.csv', [ 'name' => 'Deudores septiembre', 'country' => 'CL', 'columnMapping' => ['telefono' => 'phone', 'nombre' => 'nombre', 'deuda' => 'monto'], 'consent' => true, // declaras que tienes base legal para llamar a cada número ]); $list = $client->leadLists()->waitUntilReady($list['leadListId']); // 2. Crear la campaña a partir de una plantilla (diseño) y lanzarla. $campaign = $client->campaigns()->create([ 'general' => [ 'name' => 'Cobranza sept', 'type' => 'VOICEBOT', 'language' => 'es', 'trunkId' => 'trk-…', 'callerId' => '+56912345678', 'schedule' => ['timezone' => 'America/Santiago', 'windowStart' => '09:00', 'windowEnd' => '20:00', 'days' => [1, 2, 3, 4, 5]], 'dialingRules' => ['maxAttempts' => 3, 'retryIntervalMin' => 60, 'maxConcurrency' => 10], ], 'templateId' => 'tpl-…', 'leadListId' => $list['leadListId'], ], 'crm-batch-2026-09-27'); // clave de idempotencia opcional $estimate = $client->campaigns()->estimate($campaign['campaignId']); if ($estimate['canLaunch']) { $client->campaigns()->launch($campaign['campaignId']); // o launch($id, '2026-09-28T12:00:00Z') } // 3. Resultados: paginar llamadas, exportar o recibir webhooks. foreach ($client->calls()->list(campaignId: $campaign['campaignId'], limit: 100) as $call) { echo "{$call['phone']} {$call['disposition']} {$call['resultCode']}\n"; } $job = $client->exports()->create('CSV', ['campaignId' => $campaign['campaignId']]); $job = $client->exports()->waitUntilDone($job['jobId']); echo $job['fileUrl'];
El flujo completo con manejo de errores está en examples/run-campaign.php.
Opciones del cliente
$client = new Client('vx_live_…', [ 'baseUrl' => 'https://api-dev.callstorm.ai/v1/ext', // por defecto https://api.callstorm.ai/v1/ext 'timeout' => 30, // segundos por petición 'connectTimeout' => 10, // segundos para conectar 'maxRetries' => 2, // reintentos tras el primer intento (ver "Buenas prácticas") 'userAgent' => 'mi-crm/2.0 callstorm-php/0.1.0', 'useBearer' => false, // true => Authorization: Bearer vx_… en lugar de X-Api-Key 'transport' => null, // instancia de Callstorm\Http\Transport (por defecto CurlTransport) ]);
Todas las respuestas se devuelven como arrays asociativos con las mismas claves que el JSON de la API.
Autenticación y scopes
Las claves se crean en Settings › Integrations › API keys de la aplicación web (permiso settings:integrations).
El token (vx_live_… en producción, vx_test_… en el resto de ambientes) se muestra una sola vez.
El SDK envía X-Api-Key: vx_… por defecto; con useBearer => true envía Authorization: Bearer vx_….
Cada clave tiene scopes (los mismos permisos que un usuario). Si falta el scope que requiere una ruta, la API
responde 403 FORBIDDEN y el SDK lanza ForbiddenException; requiredScope() indica cuál falta cuando la API lo
incluye. Scopes usados por los recursos:
| Scope | Rutas |
|---|---|
campaign:read |
listar/leer campañas, estimación, llamadas |
campaign:write |
crear, editar, borrar, pausar, reanudar, detener, archivar, duplicar campañas |
campaign:launch |
lanzar campañas |
templates:write |
crear, editar, borrar, duplicar plantillas |
leads:write |
presign de archivos, crear y borrar listas |
dnc:write |
añadir y quitar números de la lista de no llamar |
reports:export |
listar y crear exportaciones |
settings:telephony |
troncales SIP, caller IDs, agentes y el token de sesión del widget de agente |
settings:integrations |
webhooks y claves de API |
credits:buy |
iniciar un checkout de créditos |
Una clave de API nunca puede crear ni revocar claves:
apiKeys()está pensado para sesiones de usuario con ese permiso.
$me = $client->me(); // ['user' => […], 'account' => […]] $dash = $client->dashboard(); // cifras del panel
Recursos
Cada recurso se obtiene desde el cliente y sus métodos corresponden 1:1 con una ruta. Los métodos de escritura
aceptan un último parámetro opcional $idempotencyKey.
Campañas — $client->campaigns()
$campaigns = $client->campaigns(); $page = $campaigns->list(status: 'RUNNING', type: 'VOICEBOT', from: '2026-09-01', to: '2026-09-30', q: 'cobranza', limit: 50); $c = $campaigns->create($draftInput, 'idem-1'); // POST /campaigns (estado DRAFT) $c = $campaigns->get('cmp-1'); // GET /campaigns/{id} $c = $campaigns->update('cmp-1', $draftInput); // PUT /campaigns/{id} (solo DRAFT) $campaigns->delete('cmp-1'); // DELETE /campaigns/{id} $e = $campaigns->estimate('cmp-1'); // ['micros', 'canLaunch', 'reasons', …] $c = $campaigns->launch('cmp-1'); // ahora $c = $campaigns->launch('cmp-1', '2026-09-28T12:00:00Z', 'launch-1'); // programada $c = $campaigns->pause('cmp-1'); $c = $campaigns->resume('cmp-1'); $c = $campaigns->stop('cmp-1'); $c = $campaigns->archive('cmp-1'); $copy = $campaigns->duplicate('cmp-1');
$draftInput sigue el esquema CampaignDraftInput del OpenAPI: general (nombre, tipo, idioma, troncal,
caller ID, schedule, dialingRules), y templateId o config inline, más leadListId.
Plantillas — $client->templates()
$templates = $client->templates(); $all = $templates->list(); // array completo (sin paginar) $ai = $templates->list('AI_AGENT'); // filtra por tipo: AUDIO | VOICEBOT | AI_AGENT $t = $templates->create(['type' => 'VOICEBOT', 'name' => 'Recordatorio', 'language' => 'es', 'payload' => $config]); $t = $templates->get('tpl-1'); $t = $templates->update('tpl-1', ['name' => 'Nuevo nombre']); // PATCH (parcial) $templates->delete('tpl-1'); // 409 IN_USE si una campaña la usa $t = $templates->duplicate('tpl-1');
Listas de contactos — $client->leadLists()
Varios teléfonos por contacto: mapea las columnas adicionales como
phone2…phone5encolumnMapping; la campaña los marca en orden mientras no haya contacto (dialingRules.maxAttemptsaplica por número).
$lists = $client->leadLists(); $all = $lists->list(); // array completo $key = $lists->uploadFile('deudores.csv'); // presign + PUT; devuelve sourceFileKey $l = $lists->create(['name' => '…', 'country' => 'CL', 'sourceFileKey' => $key, 'columnMapping' => […], 'consent' => true]); $l = $lists->createFromFile('deudores.xlsx', ['name' => '…', 'country' => 'CL', 'columnMapping' => […]]); $l = $lists->get('ll-1'); // status: UPLOADED | PROCESSING | READY | ERROR $l = $lists->waitUntilReady('ll-1', timeoutSec: 300, intervalSec: 3); $lists->delete('ll-1'); foreach ($lists->leads('ll-1', limit: 500) as $lead) { … } // paginado
Archivos — $client->files()
$files = $client->files(); $p = $files->presign('audio', 'saludo.mp3', 'audio/mpeg', filesize('saludo.mp3')); // ['uploadUrl', 'key', …] $key = $files->upload('saludo.mp3', 'audio'); // presign + subida; devuelve key
Lista de no llamar — $client->dnc()
$dnc = $client->dnc(); foreach ($dnc->list(q: '+569', limit: 200) as $entry) { … } // paginado $dnc->add(['+56912345678', ['phoneE164' => '+56987654321', 'reason' => 'Solicitud del cliente']]); $dnc->remove('+56912345678');
Llamadas — $client->calls()
$calls = $client->calls(); $paginator = $calls->list( campaignId: 'cmp-1', type: null, from: '2026-09-01T00:00:00Z', to: null, disposition: 'ANSWERED', resultCode: 'PROMISE', phone: null, sort: Calls::SORT_NEWEST, limit: 100 ); $detail = $calls->get('call-1'); // CallDetail: transcript, answers, dtmfEvents, recordingUrl (prefirmada, 5 min)
Exportaciones — $client->exports()
$exports = $client->exports(); $jobs = $exports->list(); $job = $exports->create('CSV', ['campaignId' => 'cmp-1', 'from' => '2026-09-01', 'to' => '2026-09-30']); $job = $exports->get($job['jobId']); // PENDING | PROCESSING | READY | ERROR $job = $exports->waitUntilDone($job['jobId']); // devuelve el job READY con fileUrl
Cuenta: celdas, troncales y caller IDs — $client->account()
$account = $client->account(); $cells = $account->cells(); $trunks = $account->trunks(); $t = $account->createTrunk(['name' => 'Proveedor', 'ip' => '203.0.113.10', 'channels' => 30, 'cps' => 5]); $t = $account->trunk('trk-1'); $t = $account->updateTrunk('trk-1', ['channels' => 60]); $account->deleteTrunk('trk-1'); $r = $account->testTrunk('trk-1'); // ['ok' => true, 'latencyMs' => 42] $ids = $account->callerIds(); $c = $account->createCallerId('+56229876543', 'CL', 'trk-1', 'Principal'); $c = $account->updateCallerId('+56229876543', 'trk-2', 'Backup'); $account->deleteCallerId('+56229876543');
Créditos — $client->credits()
$credits = $client->credits(); foreach ($credits->ledger(kind: 'CONSUMPTION', limit: 100) as $entry) { … } // paginado $payments = $credits->payments(); $packages = $credits->packages(); $pricing = $credits->pricing(); $checkout = $credits->checkout('pkg-1'); // ['checkoutUrl' => 'https://checkout.stripe.com/…']
Webhooks — $client->webhooks()
$hooks = $client->webhooks(); $list = $hooks->list(); $hook = $hooks->create('https://mi-crm.com/webhooks/voxcore', [Webhooks::EVENT_CALL_COMPLETED, Webhooks::EVENT_LEAD_LIST_PROCESSED]); $secret = $hook['secret']; // ¡se devuelve una sola vez! guárdalo para verificar firmas $hooks->delete($hook['webhookId']);
Claves de API — $client->apiKeys()
$keys = $client->apiKeys(); $all = $keys->list(); $key = $keys->create('CRM', ['campaign:read', 'campaign:write'], expiresAt: '2027-01-01T00:00:00Z', rateLimitPerMin: 1200); $token = $key['token']; // se devuelve una sola vez $keys->delete($key['keyId']);
Agentes y consola embebida — $client->agents()
$agents = $client->agents(); $all = $agents->list(); // presencia incluida (status, pauseReason, campaignId, currentCall…) $ana = $agents->create(['name' => 'Ana Pérez', 'email' => 'ana@empresa.cl', 'extension' => '101'], 'agent-ana'); $agents->update($ana['agentId'], ['extension' => '102']); $agents->setStatus($ana['agentId'], 'PAUSED', 'SUPERVISOR'); // supervisor: AVAILABLE | PAUSED (+ motivo) | OFFLINE $session = $agents->sessionToken($ana['agentId'], 8 * 3600); // token para la consola embebida (ver sección siguiente) $agents->delete($ana['agentId']);
Acceso de bajo nivel
Si necesitas una ruta que aún no tiene método, usa los verbos del cliente:
$client->get('/campaigns', ['status' => 'RUNNING']); $client->post('/campaigns/cmp-1/launch', [], ['startAt' => '…'], ['idempotencyKey' => 'k']); $client->put(…); $client->patch(…); $client->delete(…);
Paginación
Las colecciones grandes (campaigns, calls, dnc, lead-lists/{id}/leads, credits/ledger) devuelven
{items, nextCursor}. El SDK las envuelve en un Callstorm\Paginator que sigue nextCursor de forma perezosa:
$calls = $client->calls()->list(campaignId: 'cmp-1', limit: 100); foreach ($calls as $call) { … } // todos los elementos, página a página bajo demanda foreach ($calls->pages() as $page) { … } // ['items' => […], 'nextCursor' => '…'|null] $first = $calls->firstPage(); // una sola petición $some = $calls->all(250); // carga páginas hasta reunir 250 elementos
Cada página es una petición; la siguiente solo se pide cuando el iterador la necesita. Para procesar
colecciones muy grandes usa foreach directamente en lugar de all().
Idempotencia
Cualquier POST/PUT/DELETE admite Idempotency-Key (≤ 128 caracteres). La primera respuesta se guarda 24 h por
clave; una repetición con la misma clave devuelve la misma respuesta con Idempotent-Replayed: true sin volver a
ejecutar la operación. Los errores 5xx no se guardan, así que sí pueden reintentarse.
$campaign = $client->campaigns()->create($input, 'crm-batch-2026-09-27'); if ($client->wasReplayed()) { // la campaña ya existía: la respuesta viene del almacén de idempotencia }
Usa claves derivadas de tu propio identificador de negocio ('launch-' . $campaignId, 'export-' . $batchId…)
para que un reintento de tu sistema nunca duplique campañas, lanzamientos ni exportaciones.
Límite de tasa
Cada clave tiene un límite por minuto UTC (600 por defecto, hasta 6 000 a petición de soporte). Toda respuesta
trae X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset:
$client->get('/me'); $rate = $client->rateLimit(); // ['limit' => 600, 'remaining' => 599, 'resetAt' => '2026-10-03T12:01:00Z']
Al superar el límite la API responde 429 RATE_LIMITED con details.resetAt. El SDK espera automáticamente
hasta resetAt (máximo 60 s) y reintenta, hasta maxRetries veces. Si se agotan los reintentos lanza
RateLimitedException, cuyo resetAt() y retryAfterSeconds() te permiten esperar por tu cuenta.
Errores
Toda respuesta de error tiene la forma {code, message, details} y se convierte en una excepción de
Callstorm\Exception\. Todas descienden de CallstormException; las de la API, de ApiException, que expone
status(), apiCode(), getMessage(), details(), requestId() e idempotentReplayed().
| HTTP | code |
Excepción | Extras |
|---|---|---|---|
| 400 | VALIDATION_ERROR |
ValidationException |
issues() → [{path, message}, …] |
| 401 | UNAUTHORIZED |
UnauthorizedException |
clave ausente, revocada o caducada |
| 402 | INSUFFICIENT_CREDITS |
InsufficientCreditsException |
|
| 403 | FORBIDDEN |
ForbiddenException |
requiredScope() |
| 404 | NOT_FOUND |
NotFoundException |
|
| 409 | INVALID_TRANSITION, NOT_DRAFT, IN_USE |
ConflictException |
distingue con apiCode() |
| 413 | PAYLOAD_TOO_LARGE |
PayloadTooLargeException |
|
| 429 | RATE_LIMITED |
RateLimitedException |
resetAt(), retryAfterSeconds() |
| otros 4xx/5xx | — | ApiException |
|
| sin respuesta (red, DNS, timeout) | — | TransportException |
Además: TimeoutException y ProcessingFailedException (helpers waitUntil*, ambas con resource()),
InvalidSignatureException (webhooks).
use Callstorm\Exception\{ApiException, ConflictException, RateLimitedException, TransportException, ValidationException}; try { $client->campaigns()->launch($id); } catch (ValidationException $e) { foreach ($e->issues() as $issue) { echo "{$issue['path']}: {$issue['message']}\n"; } } catch (ConflictException $e) { if ($e->apiCode() === 'INVALID_TRANSITION') { /* ya estaba lanzada */ } } catch (RateLimitedException $e) { sleep((int) ceil($e->retryAfterSeconds())); } catch (ApiException $e) { error_log(sprintf('%d %s %s (request %s)', $e->status(), $e->apiCode(), $e->getMessage(), $e->requestId())); } catch (TransportException $e) { // sin conectividad }
Subida de listas de contactos
El flujo de la API es: POST /files/presign → PUT del archivo a uploadUrl (almacenamiento S3) →
POST /lead-lists con sourceFileKey → esperar status = READY. El SDK lo encapsula:
$lists = $client->leadLists(); // Paso a paso $key = $lists->uploadFile('deudores.csv'); // infiere text/csv por la extensión $key = $lists->uploadFile('deudores.xlsx', 'lote.xlsx'); // nombre remoto distinto $key = $lists->uploadFile('/tmp/x', 'lote.csv', 'text/csv'); // tipo explícito $list = $lists->create([ 'name' => 'Deudores', 'country' => 'CL', 'sourceFileKey' => $key, 'columnMapping' => ['telefono' => 'phone', 'nombre' => 'nombre', 'deuda' => 'monto'], 'consent' => true, ]); // O todo en uno $list = $lists->createFromFile('deudores.csv', ['name' => 'Deudores', 'country' => 'CL', 'columnMapping' => […]]); // El procesamiento es asíncrono try { $list = $lists->waitUntilReady($list['leadListId'], 300, 3); echo "{$list['validRows']} válidos, {$list['rejectedRows']} rechazados"; } catch (\Callstorm\Exception\ProcessingFailedException $e) { // status ERROR: revisa $e->resource()['rejectedFileUrl'] si existe } catch (\Callstorm\Exception\TimeoutException $e) { // sigue procesando; vuelve a consultar más tarde o suscríbete a lead_list.processed }
columnMapping asigna cada columna del archivo a phone o al nombre de una variable disponible en la
campaña ({{nombre}}, {{monto}}…). consent: true es obligatorio y queda registrado con la clave y la hora.
Webhooks
Registra la URL (HTTPS) y los eventos con webhooks()->create(); la respuesta incluye secret una única
vez. Cada entrega es un POST con cabeceras X-Voxcore-Event, X-Voxcore-Delivery y
X-Voxcore-Signature: sha256=<hex HMAC-SHA256(secret, cuerpo crudo)>, y un cuerpo {id, type, time, data}.
Eventos: call.completed, campaign.status_changed (y sus alias campaign.started, campaign.completed,
campaign.paused), lead_list.processed, balance.low (alias credits.low), payment.succeeded. Los esquemas
de data están en docs/PUBLIC_API.md y en x-webhooks del OpenAPI.
Verificación de firma
use Callstorm\Webhook\Signature; $valid = Signature::verify($rawBody, $_SERVER['HTTP_X_VOXCORE_SIGNATURE'] ?? '', $secret);
La comparación es en tiempo constante y acepta la firma con o sin el prefijo sha256=. Calcula siempre sobre el
cuerpo crudo (php://input), nunca sobre el JSON re-serializado.
Ejemplo en PHP plano
use Callstorm\Exception\InvalidSignatureException; use Callstorm\Webhook\Event; try { $event = Event::fromGlobals($secret); // lee php://input y las cabeceras, verifica la firma } catch (InvalidSignatureException $e) { http_response_code(401); exit; } http_response_code(200); // responde en < 8 s; procesa después if ($event->isCallCompleted()) { $call = $event->data; // callId, campaignId, phone, disposition, resultCode, durationSec, variables… } elseif ($event->isCampaignStatusChanged()) { // $event->data['from'], ['to'], ['stats'] } elseif ($event->isLeadListProcessed()) { // $event->data['leadListId'], ['validRows'] }
Event expone id, type, time, data (y payload completo) más isCallCompleted(),
isCampaignStatusChanged(), isLeadListProcessed(), isBalanceLow(), isPaymentSucceeded(). Las entregas se
reintentan hasta 3 veces: usa id para descartar duplicados. Ejemplo completo en
examples/webhook-receiver.php.
Laravel / Symfony
// Laravel (routes/api.php => sin CSRF). Las cabeceras PSR-7 (string[]) se aceptan tal cual. public function __invoke(Request $request): JsonResponse { try { $event = Event::fromRequest($request->getContent(), $request->headers->all(), config('services.voxcore.webhook_secret')); } catch (InvalidSignatureException) { return response()->json(['error' => 'invalid signature'], 401); } ProcessCallstormEvent::dispatch($event->toArray()); return response()->json(['status' => 'queued'], 202); }
// Symfony $event = Event::fromRequest($request->getContent(), $request->headers->all(), $secret);
Controlador completo en examples/laravel-webhook-controller.php.
Consola de agente en tu CRM (widget)
Tus agentes pueden trabajar sin salir de tu CRM: la consola de agente de Callstorm (conectarse, pausarse con motivo, elegir la campaña que atienden, recibir y hacer llamadas con el softphone del navegador, marcar en modo manual o vista previa y calificar cada llamada) se incrusta en tu página y tu backend PHP solo tiene que pedir un token de sesión por agente.
- Alta del agente (una vez): en Configuración → Agentes de Callstorm o con
$client->agents()->create([...]). Guarda elagentId(ag-…) junto al usuario de tu CRM. - Backend, al abrir la página del agente: pide el token con la clave de API (scope
settings:telephony).
$session = $client->agents()->sessionToken($agentId, ttlSec: 8 * 3600, idempotencyKey: 'crm-session-' . session_id()); // ['token' => 'vxa_live_…', 'agentId' => 'ag-…', 'expiresAt' => '…', 'embedUrl' => 'https://app.callstorm.ai/embed/agent'] $loaderUrl = \Callstorm\Resource\Agents::loaderUrl($session); // https://app.callstorm.ai/embed/voxcore-agent.js
- Página del CRM: carga el loader y monta el widget con el token (nunca en la URL ni en logs).
<div id="voxcore-agent"></div> <script src="https://app.callstorm.ai/embed/voxcore-agent.js"></script> <script> var widget = CallstormAgent.mount({ container: '#voxcore-agent', token: '<?= $session['token'] ?>', lang: 'es', height: '760px' }); widget.on('call.ringing', function (e) { abrirFicha(e.call); }); // teléfono, campaña y lead con sus variables widget.on('call.connected', function (e) { abrirFicha(e.call); }); // en colas de conferencia no hay timbrado previo widget.on('call.updated', function (e) { abrirFicha(e.call); }); // el lead llegó un instante después del evento widget.on('call.ended', function (e) { registrar(e.call); }); // transactionId, talkSec, disposition widget.on('status', function (e) { presencia(e.agent); }); // AVAILABLE / PAUSED (+ motivo) / BUSY / … widget.on('session.expired', function () { location.reload(); }); // el backend emite otro token </script>
- Eventos:
ready,status,call.ringing,call.connected,call.updated,call.ended,session.expired(o*). - Acciones desde la página:
widget.setStatus('AVAILABLE' | 'PAUSED' | 'OFFLINE', motivo?),widget.join(campaignId | null),widget.answer(),widget.hangup(),widget.setToken(token)(renovación),widget.destroy(). - El token actúa solo como ese agente y solo en las rutas de la consola; dura 8 h por defecto (5 min – 12 h) y cada
emisión queda auditada. Sin loader, también sirve un iframe a mano:
<iframe src="{embedUrl}#token={token}&lang=es" allow="microphone; autoplay">(el fragmento no llega al servidor y la página lo retira al cargar). - El audio es WebRTC entre el navegador del agente y el SBC de Callstorm: tu servidor PHP no participa en la llamada.
El iframe necesita el permiso de micrófono (
allow="microphone; autoplay", que el loader ya pone).
Ejemplo completo (PHP plano y Laravel): examples/crm-agent-widget.php.
Buenas prácticas
Reintentos. El cliente reintenta hasta maxRetries (2 por defecto):
429: espera hastaresetAt(tope 60 s) y reintenta, con cualquier verbo — la petición fue rechazada antes de ejecutarse, por lo que repetirla es seguro.- Errores de red y
502/503/504: backoff exponencial (0,5 s, 1 s, 2 s… tope 8 s) solo paraGETo para escrituras que llevanIdempotency-Key. Una escritura sin clave nunca se reintenta automáticamente: pásale una clave siempre que la operación no deba duplicarse. - Pon
maxRetries => 0si prefieres gestionar los reintentos en tu propia cola.
Claves. Guarda el token en un gestor de secretos o variable de entorno, nunca en el código ni en el repositorio. Crea una clave por integración con los scopes mínimos, con caducidad, y rota: crea la nueva, despliega, revoca la antigua. Si se filtra, revócala en la web (efecto inmediato) y crea otra; el token no se puede recuperar.
Ambientes. Usa claves vx_test_… y baseUrl => 'https://api-dev.callstorm.ai/v1/ext' en desarrollo, y
vx_live_… con la URL por defecto en producción. Separa también los webhooks por ambiente (secretos distintos).
Webhooks. Responde 2xx en menos de 8 s y procesa en segundo plano; descarta entregas repetidas por id;
exige HTTPS; verifica siempre la firma antes de leer el cuerpo.
Procesos largos. Para listas grandes o exportaciones, prefiere los webhooks lead_list.processed a sondear
con waitUntil* desde una petición web; si sondeas, hazlo desde un worker con un timeoutSec razonable.
Logs. Registra requestId() de las excepciones para que soporte pueda localizar la petición.
Versionado y compatibilidad
- Versionado semántico. Mientras la versión sea
0.x, los cambios incompatibles se anuncian en el CHANGELOG en versiones menores. - Compatible con PHP 8.1, 8.2 y 8.3 (CI en 8.1 y 8.3). Sin dependencias de terceros en producción.
- Las respuestas son arrays: los campos nuevos que añada la API aparecen automáticamente sin actualizar el SDK.
- Especificación de referencia:
https://api.callstorm.ai/v1/public/openapi.json(misma versión que sirve la API).
Desarrollo del SDK
cd sdk/php composer install composer lint # php -l en todos los archivos composer test # PHPUnit 10
Licencia MIT — IDTecnology / Callstorm.