Documentation

The manual for your board

One page, three lanes: start here, hand the second to whoever handles the cases, and send the third to whoever writes the code.

START HERE

For the administrator

Activate the plan, invite your team, set your case prefix and manage billing.

Getting started guide →
YOUR WEBSITE

For the web publisher

Three embeddable widgets, one line of script each: contact, case opening and the end-customer board.

Widgets →
INTEGRATION

For the integrator

REST API with its own credentials, signed webhooks and incremental sync.

API reference →

What the Board is

A multi-brand support board: you serve YOUR customers, under YOUR brand, on our infrastructure.

The Support Board is a single case queue for your operation: your customers open cases from your website (widgets), through the API or by email, and your team handles them from a panel of their own at dash.elportaldelcliente.com. Every case comes in, moves forward and escalates while nobody is awake — but none concludes without a name behind it.

Your board is an isolated tenant: your cases, your end customers and your agents never mix with anyone else's. Isolation is part of the system's design, not a setting someone could forget. If our staff touches one of your cases as last-level support, the access is recorded and you can look it up.

Your customers don't need an account with us. They talk to your brand: the widget, your website and your email. The one who holds the account (and the control) is you.

Getting started

From zero to operating, in five steps.

  1. Create your account at dash.elportaldelcliente.com.
  2. Activate a plan. It is paid from your account balance (the same wallet used across the ecosystem); if you are short, the screen offers to top up exactly the difference.
  3. Invite your agents in the “Agents” tab: email and role (agent or administrator). You will receive an access link to hand to them.
  4. Turn on a widget in “Widgets & API”: register your website's domain as an authorized origin, copy the script and paste it into your site.
  5. Integrating by code? Issue your tab_live_ credential in the same tab and follow the API reference on this page.
Your case prefix: in “Widgets & API” you can set 4 letters of your own (for example ACME) and your cases are numbered ACME-1044. Cases already issued are never renumbered.

The board panel

The tenant panel at dash.elportaldelcliente.com, tab by tab.

TabWhat it does
OverviewYour subscription status, expiration, renewal and auto-renewal. This is also where you cancel.
AgentsInvite by email with the agent or administrator role; change roles; remove access. The owner stays protected.
Widgets & APICase prefix, authorized origins (CORS), the three widgets with their script, and the API credentials.
Provider accessThe audit: every access by our staff to your cases — who, what and when.

The agents you invite see and handle your board's cases; administrators additionally manage the subscription, the team and the integrations.

Widgets for your website

Three widgets, one line of script each. No third-party iframes, no tracking cookies.

In your panel's “Widgets & API” tab: turn the widget on, register your site's domain as an authorized origin and copy the generated script. The data-color parameter accepts your brand color.

<script src="https://api.elportaldelcliente.com/api/tablero/widget/tablero.js"
        data-tenant="su-tablero" data-widget="contacto" data-color="#a177ff"></script>
data-widgetWhat it doesRequires
contactoContact form: name, email and message. Creates a case on your board.Widget on + authorized origin
abrir-casoCase opening with a subject: returns the case number to the visitor.Widget on + authorized origin
tableroEnd-customer board: they see their cases, open new ones and reply from your website.The above + a customer token (below)

Widget protections

Anonymous widgets accept 10 attempts per visitor (IP) every 10 minutes per board. If the widget is off, the origin is not authorized or the subscription has expired, the widget responds 404 widget no disponible — deliberately indistinguishable, so it never reveals your board's configuration.

The “board” widget: identifying your customer

The board widget shows each end customer only their own cases. To do that, your server redeems a short-lived token per customer: call POST /v1/clientes/:ref/token-widget with your API credential (server-side, never from the browser) and hand the token to the widget.

curl -X POST https://api.elportaldelcliente.com/api/tablero/v1/clientes/cli-842/token-widget \
  -H "Authorization: Bearer $TABLERO_KEY"

HTTP/1.1 201
{"success":true,"data":{"token":"tabw_…","expires_at":"2026-08-18 21:15:00"}}
window.TableroPortal.setToken(token);
The tabw_ token lives 15 minutes and does not refresh. When it expires, the widget will receive 401 token_vencido: redeem another. Never expose your tab_live_ credential in the browser — the short-lived token exists for exactly that reason.

API credentials

One Bearer credential per integration, with a narrow, revocable scope.

Credentials are issued in your panel (“Widgets & API” → “API credentials”). They take the form tab_live_… and are shown only once: store it in your secrets manager. We only keep its hash.

curl -H "Authorization: Bearer tab_live_…" \
  https://api.elportaldelcliente.com/api/tablero/v1/

When issuing one you can narrow its scopes. A credential without explicit scopes has access to your entire board:

ScopeGrants
casos.readRead cases and their replies
casos.writeCreate cases, reply and change status/priority
clientes.readRead your end customers
clientes.writeCreate/deactivate end customers and issue widget tokens
departamentos.manageRead and manage departments (includes IMAP mailboxes)
webhooks.manageRead and manage webhooks
accesos.readRead the provider access audit
  • A maximum of 5 active credentials per board. Revoke the ones you no longer use.
  • The credential identifies your board: never send tenant_id in the body — the API rejects it with 400.
  • With an expired subscription the credential keeps reading (GET); every write responds 402 suscripcion_vencida.
  • A revoked credential responds 401 immediately on every route.
Server-side only. The tab_live_ credential never goes in the browser, a mobile app or a repository. For the browser there is the short-lived tabw_ token.

API reference

REST over HTTPS, JSON in both directions, responses shaped {success, data}.

Conventions: request bodies go as Content-Type: application/json; dates are UTC YYYY-MM-DD HH:MM:SS; paginated listings return total, page and limit. Whatever is not yours responds 404, never 403: the API does not confirm the existence of other tenants' resources.

https://api.elportaldelcliente.com/api/tablero/v1

Root: identity, plan and usage

GET / is the contract that cannot lie: it returns who you are, which scopes this credential holds, your plan with its limits, your subscription status and how much you have used.

curl -H "Authorization: Bearer $TABLERO_KEY" \
  https://api.elportaldelcliente.com/api/tablero/v1/

{"success":true,"data":{
  "tenant":{"slug":"su-tablero","nombre":"Su Marca"},
  "scopes_de_esta_credencial":["casos.read","casos.write"],
  "plan":{"id":"tablero-base","nombre":"BASE","limites":{"max_agentes":5,"max_departamentos":5,"max_articulos_kb":100}},
  "suscripcion":{"estado":"active","current_period_end":"2027-08-18"},
  "uso":{"agentes":3,"departamentos":2},
  "recursos":["/casos","/clientes","/departamentos","/accesos","/webhooks"],
  "version":"v1"}}

End customers

An end customer is a customer of yours (not an account in our ecosystem). They are identified by external_ref: the ID that customer has in YOUR system.

MethodRouteScopeWhat it does
GET/clientesclientes.readList customers (up to 500, most recent first)
POST/clientesclientes.writeCreate or update by external_ref (idempotent upsert)
GET/clientes/:refclientes.readRead one customer by external_ref
DELETE/clientes/:refclientes.writeDeactivate (does not delete: their case history is kept)
POST/clientes/:ref/token-widgetclientes.writeIssue a short-lived token for the “board” widget (15 min)
curl -X POST https://api.elportaldelcliente.com/api/tablero/v1/clientes \
  -H "Authorization: Bearer $TABLERO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_ref":"cli-842","email":"[email protected]","name":"Maria Rojas"}'
Safe retries: POST /clientes is an upsert by external_ref — repeating the call does not duplicate customers. When creating cases, on the other hand, a network retry creates a new case: retry only when you received no response.

Cases

The central resource. A case has a status, a priority, an optional end customer and a conversation of replies. Agents' internal notes are never exposed through the API.

MethodRouteScopeWhat it does
GET/casoscasos.readList with filters: status, customer, updated_since, limit (1-100), page
POST/casoscasos.writeCreate a case (subject required; cliente_ref, priority, department_id, body/body_html optional)
GET/casos/:idcasos.readDetail with the full conversation (replies in order). Accepts the case ID or number (e.g. SOLC-1051) either way
POST/casos/:id/respuestascasos.writeReply as your team, or as the customer with en_nombre_de
PATCH/casos/:idcasos.writeChange status and/or priority

Create a case tied to an end customer:

curl -X POST https://api.elportaldelcliente.com/api/tablero/v1/casos \
  -H "Authorization: Bearer $TABLERO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject":"No puedo entrar al panel","cliente_ref":"cli-842","priority":"high",
       "body":"Al iniciar sesion la pagina queda en blanco."}'

HTTP/1.1 201
{"success":true,"data":{"id":"…","numero":"SOLC-1051"}}

List with filters, and incremental sync with updated_since (ISO 8601): in that mode the order is updated_at ascending, designed to advance page by page without missing changes. Store the highest updated_at you processed and use it as the cursor for the next pass.

curl -H "Authorization: Bearer $TABLERO_KEY" \
  "https://api.elportaldelcliente.com/api/tablero/v1/casos?estado=open&cliente=cli-842&limit=50&page=1"

# incremental sync (order: updated_at ascending)
curl -H "Authorization: Bearer $TABLERO_KEY" \
  "https://api.elportaldelcliente.com/api/tablero/v1/casos?updated_since=2026-08-18T00:00:00Z&limit=100"
A filter that doesn't filter is worse than an error: an estado outside the valid list (or any parameter we don't recognize) responds with 400 and the detail — it's never silently ignored, nor does it return the full list without warning.

Reply to a case. Without en_nombre_de the reply signs as your team (author_type: agent); with en_nombre_de it signs as that end customer (author_type: client):

curl -X POST https://api.elportaldelcliente.com/api/tablero/v1/casos/$CASO_ID/respuestas \
  -H "Authorization: Bearer $TABLERO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"Ya lo revisamos: intente de nuevo.","autor_nombre":"Soporte Acme"}'

# ...or on behalf of the end customer:
  -d '{"body":"Sigue igual desde mi lado.","en_nombre_de":"cli-842"}'
FieldValues
statusopen · pending · resolved · closed
prioritylow · medium · high · urgent
author_typeclient · agent
Verifiable isolation: every case query passes through your tenant's scope on the server. A case ID from another board responds 404 — the API behaves as if it did not exist, because for you it does not.

Departments

Internal queues for your board (sales, billing, technical…). Each one can have its own IMAP mailbox: the board polls it and turns inbound email into cases.

MethodRouteScopeWhat it does
GET/departamentosdepartamentos.manageList departments (includes the email configuration, without the password)
POST/departamentosdepartamentos.manageCreate (name required; IMAP mailbox optional; subject to the plan limit)
PUT/departamentos/:iddepartamentos.manageEdit any field; an empty email_password deletes the credential
DELETE/departamentos/:iddepartamentos.manageArchive (existing cases are untouched)

The mailbox speaks IMAP, always (email_protocol: imap); the password is encrypted at rest and never returned. With email_polling_enabled: 1 polling runs automatically.

Access audit

Every access by our staff to your board's cases is recorded. This is the same audit as the “Provider access” tab, readable through the API:

curl -H "Authorization: Bearer $TABLERO_KEY" \
  https://api.elportaldelcliente.com/api/tablero/v1/accesos

Webhooks

Your system knows the moment it happens: every event arrives signed at your HTTPS URL.

EventFires when
caso.creadoA case is born on your board — through the API, a widget or inbound email
caso.respondidoSomeone replies: your team (from the panel or the API), or your end customer (widget, API or email)
caso.estado_cambiadoA case changes status (by your team, the API or our system)

Register as many as you need with POST /webhooks: an https URL, the events you want and a secret of at least 16 characters we will use to sign every delivery.

curl -X POST https://api.elportaldelcliente.com/api/tablero/v1/webhooks \
  -H "Authorization: Bearer $TABLERO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://su-sistema.com/hooks/tablero",
       "eventos":["caso.creado","caso.respondido","caso.estado_cambiado"],
       "secret":"un-secreto-de-al-menos-16-caracteres"}'

Each delivery is a POST with the event, the moment of emission and the minimum data needed to react. The signature covers timestamp.body:

POST /hooks/tablero
Content-Type: application/json
X-Tablero-Evento: caso.respondido
X-Tablero-Timestamp: 1765432100
X-Tablero-Firma: 3f1a…  # HMAC-SHA256(secret, timestamp + "." + body)

{"evento":"caso.respondido","emitido_at":"2026-08-18T20:15:00.000Z",
 "data":{"caso_id":"…","numero":"SOLC-1051","respuesta_id":"…","autor_tipo":"agent"}}

Verifying the signature

Reject any delivery whose signature does not verify or whose timestamp is more than 5 minutes off the clock. Compare in constant time.

// Node.js
const crypto = require('node:crypto');

function verificarFirma(rawBody, headers, secret) {
  const ts  = headers['x-tablero-timestamp'];
  const sig = headers['x-tablero-firma'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const esperado = crypto.createHmac('sha256', secret)
    .update(ts + '.' + rawBody).digest('hex');
  return sig.length === esperado.length &&
    crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(sig));
}
# Python
import hashlib, hmac, time

def verificar_firma(raw_body: bytes, headers: dict, secret: str) -> bool:
    ts  = headers.get("x-tablero-timestamp", "")
    sig = headers.get("x-tablero-firma", "")
    if abs(time.time() - float(ts or 0)) > 300:
        return False
    esperado = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, sig)

Delivery semantics (read this before you rely on it)

  • One attempt per event, no retries. If your endpoint does not respond 2xx within 10 seconds, that delivery is lost — the real state can always be re-read through the API.
  • Your safety net is incremental sync: a periodic pass of GET /casos?updated_since=… recovers any lost event.
  • GET /webhooks shows each webhook's fail_count, last_ok_at and last_error_at: check them when debugging.
  • Events are emitted no matter the channel: API, widgets, your team's panel, our staff or your end customer's inbound email.

Errors

JSON errors with success:false; the HTTP status code rules.

{"success":false,"error":"…"}
HTTPMeaning
400Invalid request: a required field is missing or a value fails validation
401Credential missing, invalid or revoked
402Subscription expired: read-only until renewal (suscripcion_vencida)
403The credential lacks the scope the endpoint requires
404It does not exist for you: other tenants' resources and nonexistent routes respond the same
409Conflict: plan limit reached or slug already in use
429Too many attempts (anonymous widgets: 10 per IP every 10 minutes)
500Our error; if it persists, write to us with the exact time

The error messages are human-readable text and may change. Branch your code on the HTTP status code and on these stable slugs:

Stable slugMeaning
suscripcion_vencida402 on writes with the subscription expired
token_vencido401 from the “board” widget when the tabw_ token expired: redeem another
saldo_insuficiente402 from the panel when activating/renewing without enough balance

Plans

Five annual plans, the same board. The difference is how many people and how much structure fit.

Current prices are published on the home page and through the public API (GET /api/public/tablero/plans). Every plan includes unlimited cases, widgets, API, webhooks and the access audit.

PlanAgentsDepartmentsKB articles
ZERO2225
BASE55100
PLUS1010250
PRO2015500
MAX50251000
No free trial, on purpose. The ZERO plan exists to start small with real commitment. Activation debits the full year from your account balance.

When your subscription expires there is a grace period; after that the board goes read-only (GETs keep responding, writes return 402). Your data is not deleted.

Operational limits

  • Cases: unlimited on every plan. Listings accept a limit of 1 to 100 per page.
  • End customers and audit: listings return up to 500 rows (most recent first). Use GET /clientes/:ref for point reads.
  • Credentials: up to 5 active per board.
  • Anonymous widgets: 10 attempts per visitor (IP) every 10 minutes.
  • Attachments: the API does not accept files yet; inbound email attachments are kept with the case.

The trunk does not enforce a hard call quota today; operate at a reasonable pace (pauses between pages, retries with exponential backoff). If an integration needs guaranteed volume, let's talk first.