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.
For the administrator
Activate the plan, invite your team, set your case prefix and manage billing.
Getting started guide →For the web publisher
Three embeddable widgets, one line of script each: contact, case opening and the end-customer board.
Widgets →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.
Getting started
From zero to operating, in five steps.
- Create your account at dash.elportaldelcliente.com.
- 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.
- Invite your agents in the “Agents” tab: email and role (agent or administrator). You will receive an access link to hand to them.
- 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.
- Integrating by code? Issue your
tab_live_credential in the same tab and follow the API reference on this page.
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.
| Tab | What it does |
|---|---|
Overview | Your subscription status, expiration, renewal and auto-renewal. This is also where you cancel. |
Agents | Invite by email with the agent or administrator role; change roles; remove access. The owner stays protected. |
Widgets & API | Case prefix, authorized origins (CORS), the three widgets with their script, and the API credentials. |
Provider access | The 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-widget | What it does | Requires |
|---|---|---|
contacto | Contact form: name, email and message. Creates a case on your board. | Widget on + authorized origin |
abrir-caso | Case opening with a subject: returns the case number to the visitor. | Widget on + authorized origin |
tablero | End-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);
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:
| Scope | Grants |
|---|---|
casos.read | Read cases and their replies |
casos.write | Create cases, reply and change status/priority |
clientes.read | Read your end customers |
clientes.write | Create/deactivate end customers and issue widget tokens |
departamentos.manage | Read and manage departments (includes IMAP mailboxes) |
webhooks.manage | Read and manage webhooks |
accesos.read | Read 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_idin 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.
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.
| Method | Route | Scope | What it does |
|---|---|---|---|
| GET | /clientes | clientes.read | List customers (up to 500, most recent first) |
| POST | /clientes | clientes.write | Create or update by external_ref (idempotent upsert) |
| GET | /clientes/:ref | clientes.read | Read one customer by external_ref |
| DELETE | /clientes/:ref | clientes.write | Deactivate (does not delete: their case history is kept) |
| POST | /clientes/:ref/token-widget | clientes.write | Issue 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"}'
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.
| Method | Route | Scope | What it does |
|---|---|---|---|
| GET | /casos | casos.read | List with filters: status, customer, updated_since, limit (1-100), page |
| POST | /casos | casos.write | Create a case (subject required; cliente_ref, priority, department_id, body/body_html optional) |
| GET | /casos/:id | casos.read | Detail with the full conversation (replies in order). Accepts the case ID or number (e.g. SOLC-1051) either way |
| POST | /casos/:id/respuestas | casos.write | Reply as your team, or as the customer with en_nombre_de |
| PATCH | /casos/:id | casos.write | Change 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"
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"}'
| Field | Values |
|---|---|
status | open · pending · resolved · closed |
priority | low · medium · high · urgent |
author_type | client · agent |
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.
| Method | Route | Scope | What it does |
|---|---|---|---|
| GET | /departamentos | departamentos.manage | List departments (includes the email configuration, without the password) |
| POST | /departamentos | departamentos.manage | Create (name required; IMAP mailbox optional; subject to the plan limit) |
| PUT | /departamentos/:id | departamentos.manage | Edit any field; an empty email_password deletes the credential |
| DELETE | /departamentos/:id | departamentos.manage | Archive (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.
| Event | Fires when |
|---|---|
caso.creado | A case is born on your board — through the API, a widget or inbound email |
caso.respondido | Someone replies: your team (from the panel or the API), or your end customer (widget, API or email) |
caso.estado_cambiado | A 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 /webhooksshows each webhook'sfail_count,last_ok_atandlast_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":"…"}
| HTTP | Meaning |
|---|---|
400 | Invalid request: a required field is missing or a value fails validation |
401 | Credential missing, invalid or revoked |
402 | Subscription expired: read-only until renewal (suscripcion_vencida) |
403 | The credential lacks the scope the endpoint requires |
404 | It does not exist for you: other tenants' resources and nonexistent routes respond the same |
409 | Conflict: plan limit reached or slug already in use |
429 | Too many attempts (anonymous widgets: 10 per IP every 10 minutes) |
500 | Our 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 slug | Meaning |
|---|---|
suscripcion_vencida | 402 on writes with the subscription expired |
token_vencido | 401 from the “board” widget when the tabw_ token expired: redeem another |
saldo_insuficiente | 402 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.
| Plan | Agents | Departments | KB articles |
|---|---|---|---|
| ZERO | 2 | 2 | 25 |
| BASE | 5 | 5 | 100 |
| PLUS | 10 | 10 | 250 |
| PRO | 20 | 15 | 500 |
| MAX | 50 | 25 | 1000 |
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
limitof 1 to 100 per page. - End customers and audit: listings return up to 500 rows (most recent first). Use
GET /clientes/:reffor 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.