← Tous les utilitaires

NSNatAPI

Version 26.1 (build 185)

Cette documentation n’existe pas encore en français : affichage en anglais.

NSNatAPI
Sommaire

What is NSNatAPI

nsnatapi (NSNatAPI) is Nacarsoft’s gateway that exposes Natural subprograms as REST endpoints (GET/POST/PUT/DELETE) for machine clients (Java, scripts, cron, microservices), with no EntireX or RPC broker. Each endpoint is defined in the console, protected with Bearer tokens and runs in one of two modes:

  • batch (default): one Natural process per call (~30–90 ms cold start).
  • session (opt-in): a pool of reusable persistent Natural sessions (~1–5 ms per call) for high-frequency traffic.

Program names, hosts, tokens and accounts in the examples are fictitious.

NSNatAPI architecture: consuming services → nsnatapi → Natural → Adabas

Commercial deck: PDF (ES) · PPTX (ES) · PDF (EN) · PPTX (EN)

Requirements

  • Linux (RHEL 8/9).
  • Software AG Natural + Adabas client (sagenv) with the buffer pool running.
  • FUSER with the business subprograms (read and write, to deploy).
  • Reverse proxy with HTTPS in production.

Getting started

nsnatapi init ./data                 # config.yaml (0600), db, admin user
nsnatapi start ./data                # daemon, port 11032 by default
nsnatapi status ./data

Web console at http://host:11032/ (login admin; change the password after first access). The nsnatapi passwd command restores access.

Recommended order: 1) write the Natural subprogram, 2) create the endpoint, 3) press Deploy, 4) create the token with --endpoint <name> (that name must already exist).

The Natural module, step by step

The only code you write is the .NSN subprogram, following the house convention: exactly three parameters.

The minimal contract

0010* MIECHO - minimal echo (working template)
0020DEFINE DATA PARAMETER
00301 #PARM-IN  (A) DYNAMIC   /* input: one params line (<=240 chars) */
00401 #PARM-OUT (A) DYNAMIC   /* output: reply (see response modes) */
00501 #RESP     (I4)          /* 0 = ok, >0 = business error */
0060END-DEFINE
0070RESET #PARM-OUT #RESP
0080COMPRESS 'ECHO:' #PARM-IN INTO #PARM-OUT
0090END

What each parameter carries:

ParameterDirectionContent
#PARM-IN (A) DYNAMICInputA single line with the parameters, joined with the endpoint separator (max. 240 characters)
#PARM-OUT (A) DYNAMICOutputThe business reply (text, separated fields or JSON, depending on response_mode)
#RESP (I4)Output0 = ok; >0 = business error (the client receives it as rc)

Mandatory rules

  • Program name A-Z0-9-_, max 8 characters, uppercase.
  • Always end with END (never STOP or LOGON: they would kill the batch session).
  • Stateless per call: no globals/AIVs carried over between calls.
  • Always close the unit of work: END TRANSACTION on success, BACKOUT if an update fails.
  • Deploy the .NSN through your usual process (nsdeploy/SYSOBJH); nsnatapi never recompiles your programs, it only generates and compiles its stubs/drivers.

Example 1: lookup with a business RC

A GET /v1/saldo/:cuenta where the account travels in the input line and the program decides the result with #RESP:

0010* CPOLIZA - policy lookup (template)
0020DEFINE DATA PARAMETER
00301 #PARM-IN  (A) DYNAMIC
00401 #PARM-OUT (A) DYNAMIC
00501 #RESP     (I4)
0060LOCAL
00701 #CUENTA (A8)
0080END-DEFINE
0090RESET #PARM-OUT #RESP #CUENTA
0100MOVE #PARM-IN TO #CUENTA
0110* Your Adabas READ with your view/DDM goes here; if not found:
0120IF #CUENTA = '999999'
0130  MOVE 'ACCOUNT NOT FOUND' TO #PARM-OUT
0140  MOVE 4 TO #RESP
0150ELSE
0160  COMPRESS 'BALANCE 1234.56' INTO #PARM-OUT
0170  MOVE 0 TO #RESP
0180END-IF
0190END TRANSACTION
0200END

Call and response (response_mode=text):

curl -H "Authorization: Bearer nstm_..." \
     http://host:11032/api/ext/v1/saldo/001234
{ "ok": true, "rc": 0, "output": "BALANCE 1234.56", "duration_ms": 22 }

For a missing account, HTTP is still 200 because the program ran; the business error travels in rc:

{ "ok": true, "rc": 4, "output": "ACCOUNT NOT FOUND", "duration_ms": 19 }

Example 2: POST with two fields and a separator

A POST /v1/pagos with a JSON body. The values are joined into one line (importe|concepto) that the program splits:

0010* CPAGOS - payment entry (2 fields separated by |)
0020DEFINE DATA PARAMETER
00301 #PARM-IN  (A) DYNAMIC
00401 #PARM-OUT (A) DYNAMIC
00501 #RESP     (I4)
0060LOCAL
00701 #IMPORTE  (A15)
00801 #CONCEPTO (A60)
0090END-DEFINE
0100RESET #PARM-OUT #RESP #IMPORTE #CONCEPTO
0110SEPARATE #PARM-IN INTO #IMPORTE #CONCEPTO WITH DELIMITER '|'
0120IF #IMPORTE = ' ' OR #IMPORTE = ''
0130  MOVE 'MISSING IMPORTE' TO #PARM-OUT
0140  MOVE 1 TO #RESP
0150ELSE
0160  COMPRESS 'PAYMENT RECORDED|' #IMPORTE '|' #CONCEPTO INTO #PARM-OUT
0170  MOVE 0 TO #RESP
0180END-IF
0190END TRANSACTION
0200END
curl -H "Authorization: Bearer nstm_..." -H 'Content-Type: application/json' \
     -d '{"importe":"1500","concepto":"prima"}' \
     http://host:11032/api/ext/v1/pagos
{ "ok": true, "rc": 0, "output": "PAYMENT RECORDED|1500|prima", "duration_ms": 31 }

Example 3: real JSON output

If the endpoint uses response_mode=json, the program must emit valid JSON. Note: in this Natural environment a " literal in the source compiles to '; build quotes with H'22' (or use GENERATE JSON):

0010* JECHO - returns a JSON object (response_mode=json)
0020DEFINE DATA
0030PARAMETER
00401 #PARM-IN (A) DYNAMIC
00501 #PARM-OUT (A) DYNAMIC
00601 #RESP (I4)
0070LOCAL
00801 #Q (A1)
0090END-DEFINE
0100RESET #PARM-OUT #RESP
0110MOVE H'22' TO #Q
0120COMPRESS '{' #Q 'msg' #Q ':' #Q #PARM-IN #Q '}' INTO #PARM-OUT LEAVING NO SPACE
0130END

Response: output always carries the raw text and data the already-parsed object:

{
  "ok": true,
  "rc": 0,
  "output": "{\"msg\":\"hola\"}",
  "duration_ms": 24,
  "data": { "msg": "hola" }
}

If the program emits single quotes instead of double quotes, the call fails with 502 {"ok":false,"error":"invalid json from program"}.

Creating the endpoint, step by step

1. Endpoint fields

FieldRequiredDescription
nameYesReadable name (max. 64, no control characters)
methodYesGET, POST, PUT or DELETE
path_templateYesStarts with /; captures with :name (unique, letter-first, max. 32). E.g.: /v1/saldo/:cuenta
libraryYes*Natural library (uppercase). If omitted, the environment default library is used
programYesExisting subprogram (A-Z0-9-_, max. 8)
environmentNoNatural environment (DEV/TST/PRD); empty = default environment
fields_jsonNoExtra fields in order: query on GET/DELETE, JSON body on POST/PUT. Format: [{"name":"importe","required":true}]
separatorNoA single printable ASCII character (default |)
exec_modeNobatch (default) or session
response_modeNotext (default), fields or json
response_fields_jsonfields onlyName and type of each position: [{"name":"saldo","type":"number"}]; types: text, number, int, bool, json
timeout_secNoCall timeout (0 = server default; configurable cap)

Golden mapping rule: path captures (in order) + fields (in fields_json order), joined with the separator into one line. So if the parameter already travels in the path (:cuenta), fields_json must be []: don’t repeat it there, or you will additionally be asked for the same value as a query param.

2. Create in the console

Endpoints → Create: method, template, library, environment, program, fields, separator, timeout and execution mode. Press Deploy and then Try it to test it.

Quick example: GET, template /v1/saldo/:cuenta, library NSNAT, environment DEV, program CPOLIZA, fields_json [], mode batch.

3. Create via API (admin)

# Console login (stores the 'session' cookie)
curl -s -c cookies.txt -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"********"}' \
  http://host:11032/api/auth/login

# GET endpoint with a path capture (fields_json empty: the value already comes in :cuenta)
curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"name":"saldo","method":"GET","path_template":"/v1/saldo/:cuenta",
       "library":"NSNAT","program":"CPOLIZA","environment":"DEV",
       "exec_mode":"batch","response_mode":"text","fields_json":"[]",
       "separator":"|","timeout_sec":30}' \
  http://host:11032/api/endpoints

# POST endpoint with two body fields
curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"name":"pagos","method":"POST","path_template":"/v1/pagos",
       "library":"NSNAT","program":"CPAGOS","environment":"DEV",
       "exec_mode":"batch","response_mode":"text",
       "fields_json":"[{\"name\":\"importe\",\"required\":true},{\"name\":\"concepto\",\"required\":false}]",
       "separator":"|","timeout_sec":30}' \
  http://host:11032/api/endpoints

# Deploy (the id comes in the creation response)
curl -s -b cookies.txt -X POST http://host:11032/api/endpoints/1/deploy

4. Deploy: what nsnatapi compiles

  • batch: Deploy generates and compiles a NX000042 stub (reporting mode) in the environment library. The stub does INPUT, CALLNAT '<your-program>' and writes the reply with the NSNAT:OUT:<len> / NSNAT:C:<len>:<chunk> / NSNAT:END RC=<n> sentinels.
  • session: Deploy compiles the shared NSRPDRV driver (structured mode) once per library; the pool reuses it and dispatches to your subprograms with dynamic CALLNAT.

Your .NSN files don’t change between modes. Don’t hand-edit the NXnnnnnn files: they are regenerated on every Deploy.

5. Scoped token

nsnatapi token ./data --name java-prd --endpoint saldo --days 365 \
    --cidr 10.0.0.0/8,192.168.1.10
# → nstm_xxxxxxxx…  (store it; it is shown only once)
nsnatapi token ./data --list

Without --endpoint, the token is valid for all endpoints. You can restrict by IP/CIDR, expiry and revocation, plus per-token rate limiting.

The secret is shown only once, at creation (with a Copy button in the console); afterwards it is unrecoverable —only its hash is stored— so if it is lost you rotate it from the console (Rotate button) or with POST /api/tokens/:id/rotate: same id and metadata, new secret, the old one stops working.

Handing a token to an application: 1) deployed and tested endpoint; 2) one token per app, minimal scope, expiry and CIDRs when applicable; 3) fill in the contact e-mail and the app sends a notice with a Get token button: a single-use link (48 h) showing the secret once (resendable with ✉️); 4) copy the secret once, straight into the app’s secret store (never notepad or e-mail); 5) verify with a test call under Calls; 6) on loss or leak, rotate; when the app is decommissioned, revoke.

How the params line is built

CaseWhere each value comes fromExample
GET /v1/saldo/001234, fields_json: []:cuenta capture001234
GET /v1/saldo/001234?formato=corto, fields: [formato]Capture + query001234|corto
POST /v1/pagos, body {"importe":"1500","concepto":"prima"}, fields: [importe, concepto]Body in order1500|prima
PUT /v1/saldo/001234, body {"importe":"1500"}Capture + body001234|1500

The default separator is | and must be a single printable ASCII character. The total line must not exceed 240 characters or contain line breaks.

JSON responses

All machine calls use /api/ext/... with Authorization: Bearer nstm_....

text mode (default)

Only output with the program’s raw text:

{ "ok": true, "rc": 0, "output": "BALANCE 1234.56", "duration_ms": 22 }

fields mode: typed object plus the raw text

The program returns separated positions (123456|Juan Pérez|1520.75|ACTIVA) and the endpoint declares response_mode: fields with:

[{"name":"cuenta","type":"text"},{"name":"titular","type":"text"},
 {"name":"saldo","type":"number"},{"name":"estado","type":"text"}]

Response:

{
  "ok": true,
  "rc": 0,
  "output": "123456|Juan Pérez|1520.75|ACTIVA",
  "duration_ms": 22,
  "data": {
    "cuenta": "123456",
    "titular": "Juan Pérez",
    "saldo": 1520.75,
    "estado": "ACTIVA"
  }
}

json mode: validated object

The program emits JSON (see the JECHO example above) and the response includes the object in data. If it isn’t valid JSON: 502 {"ok":false,"error":"invalid json from program"}.

Errors: common shape

All machine errors share one shape:

{ "ok": false, "error": "<message>" }
HTTPExample errorMeaning
200n/a (rc: 4, output: "ACCOUNT NOT FOUND")It ran; the business says no
400missing param cuentaMissing required field (query or body)
400json body required / invalid json bodyPOST/PUT without a valid JSON body
400params too long (max 240 chars)Param line too long
401invalid tokenMissing, revoked or expired token
404no such endpointNo active endpoint with that method + path
429rate limit exceededPer-token/IP limit reached
502natural execution failedNatural failed (detail in the call log)
502invalid json from programresponse_mode=json but the output isn’t JSON
503endpoint not deployedStub/driver Deploy missing
503server too busy, try again laterConcurrency or session pool saturated
504natural call timed outThe program exceeded timeout_sec

Calling from clients

# GET with a path capture
curl -H "Authorization: Bearer nstm_…" \
     http://host:11032/api/ext/v1/saldo/001234

# POST with a JSON body (fields defined on the endpoint)
curl -H "Authorization: Bearer nstm_…" -H 'Content-Type: application/json' \
     -d '{"importe":"1500","concepto":"prima"}' \
     http://host:11032/api/ext/v1/pagos

Minimal Java client (Java 11+, no dependencies; full example in the project’s examples/java/NatApiClient.java):

var req = HttpRequest.newBuilder(URI.create(base + "/api/ext/v1/saldo/001234"))
    .header("Authorization", "Bearer " + token).GET().build();
var body = client.send(req, BodyHandlers.ofString()).body();
// {"ok":true,"rc":0,"output":"BALANCE 1234.56","duration_ms":22}
// HTTP 200 = executed (even rc≠0); always check rc.

API reference

Sources of truth: api/openapi.yaml (English) and api/openapi.es.yaml (Spanish), OpenAPI 3.1, in the project. The app serves them at /api/docs (interactive Swagger UI, with a Try it out button), /api/openapi.json and /api/openapi.yaml, with ?lang=es|en or Accept-Language negotiation. It covers the Machine API (/api/ext/*, Bearer) and the Console API (/api/*, session cookie), with the RBAC permission (x-permission) of each operation and curl samples.

Additionally, every active endpoint shows up as its own operation (its real template, method and parameters): developers see what is available, how to call it, what it returns, and can try it with their token (fill it in at the top or pass ?token=nstm_…).

Visibility (hot-reloaded, config.yaml or Configuration → API docs):

apiDocs:
  access: private   # public (no login) | private (console session) | off (404)

Full usage guide (auth, conventions, pagination, errors, versioning): nsnatapi/doc/api.md.

Console: dashboard, e-mail and sessions

The Dashboard shows live server telemetry (CPU, memory, load, disk, process RSS/heap/goroutines, Natural slots in use and uptime), sampled from /proc with no external dependencies and refreshed every 5 s from GET /api/system/stats (admin only).

The correo section (SMTP or sendmail, with a Test email button) enables password recovery: Forgot password? sends a single-use link (60 min); unknown addresses always get the same generic response (no user enumeration). The SMTP password can come from an env var (NSNATAPI_SMTP_PASSWORD).

Console sessions rotate the refresh token on every use with reuse detection (a replayed token revokes every session); users.token_version invalidates everything on password change/reset or forced logout. On top: auth.tokenPepper (HMAC for API tokens), automatic Secure cookies, server.trustedProxies (loopback by default) for IP integrity in auditing and brute-force protection, and generic client errors.

Endpoints and modes

  • batch: clicking Deploy generates and compiles a NX000042 stub (reporting mode) in the environment library.
  • session: Deploy compiles the shared NSRPDRV driver (structured mode) once per library; the pool reuses it.

Parameter mapping: path captures (in order) + fields (query on GET/DELETE, JSON body on POST/PUT), joined with the separator into one line.

Persistent sessions

natural:
  numSessions: 4            # pool size per (environment, library)
  maxSessions: 16           # cap (real limit is NATBP MAXUSERS)
  sessionIdleSec: 600       # close sessions idle > 10 min (0 = off)
environments:
  DEV: { bp: NATBP, numSessions: 6 }
  TST: { bp: NATTST, numSessions: 2, sessionIdleSec: 300 }

Each session is a Natural process (it consumes a buffer-pool user; leave headroom in MAXUSERS). A fatal error recycles the session automatically.

Avoiding NAT3009: a persistent session that opened Adabas can expire due to non-activity (TNAA/TNAE/TNAX) and the next call fails with NAT3009 (subcode 3). With sessionIdleSec the session is closed before it expires (must be below the effective TNAE, with margin): the next request spawns a fresh session with a new Adabas connection, so the 3009 never happens. The close is recorded in the server log (recycled by idle timeout) and in the audit log (SESSION_IDLE_TIMEOUT), and counted per pool (recycled_idle).

Base vs environments: environments is the source of truth and each environment is self-contained. The top-level natural block is only the base defaults (it pre-fills new environments and acts as a fallback when none is defined); it is not an environment. defaultEnvironment picks the environment used when an endpoint does not specify one.

It is configured in the console, Configuration → Natural environments: the Session pool size (numSessions) field sets the default and each environment’s modal sets its own (the environment value overrides the global; it is the number Pre-warm spawns). The change applies live: the pool is resized (shrinking recycles surplus idle sessions). Pools are separated by environment (e.g. 3 sessions in DEV and 8 in TST, each with its own values); an endpoint with no explicit environment uses the default one. The Sessions page labels each pool with its environment and the call log records the environment (column and Environment filter in Calls). The Natural RPC session pools panel (Configuration) lists all environments and lets you New pool (per library), Edit limits, Pre-warm, Restart and Delete. Via API: GET /api/natural/sessions, POST /api/natural/sessions/warm|restart|delete; counters are also in GET /api/system/stats → natural.sessions.

The Sessions page (admin, auto-refresh every 5 s) shows each live Natural session: PID, state (idle/busy/dead), uptime, last call, calls served (transactions proxy), errors, last program, latencies (last/avg), instantaneous and average CPU (%), and memory (RSS), with Pre-warm / Restart / Kill session actions.

Usage examples

1. Quick endpoint setup (console)

Endpoints → Create: method GET, template /v1/saldo/:cuenta, library NSNAT, program CPOLIZA, environment DEV, mode batch, fields_json []. Click Deploy to compile the stub and Try it to test it.

2. The same setup via API (admin)

See “Create via API” above: fields_json stays empty because the value already travels in the :cuenta capture.

3. Session-mode endpoint (high frequency)

curl -s -b cookies.txt -H 'Content-Type: application/json' \
  -d '{"name":"saldo-session","method":"GET","path_template":"/v1/saldo-session/:cuenta",
       "library":"NSNAT","program":"CPOLIZA","exec_mode":"session",
       "fields_json":"[]"}' \
  http://host:11032/api/endpoints

# Deploy compiles the NSRPDRV driver (once per library)
curl -s -b cookies.txt -X POST http://host:11032/api/endpoints/2/deploy

4. Scoped API token with allowlist

See “Scoped token” above (create first the endpoint whose name you pass in --endpoint).

5. Client that distinguishes rc from HTTP

var body = client.send(req, BodyHandlers.ofString()).body();
// HTTP 200 + {"ok":true,"rc":4,"output":"ACCOUNT NOT FOUND",...}
// → not a technical failure: the business says the account does not exist.

6. Observability

# Per-endpoint metrics (admin, console cookie)
curl -s -b cookies.txt http://host:11032/api/stats | jq '.[] | {endpoint_id,calls,errors,avg_ms}'
# Session pool occupancy
curl -s -b cookies.txt http://host:11032/api/system/stats | jq '.natural.sessions'
# Calls filtered by library (the Calls page shows Environment, Library and PID)
curl -s -b cookies.txt 'http://host:11032/api/calls?library=NSNAT' | jq '.items[] | {environment,library,exec_mode,session_pid,http_status}'

Configuration (config.yaml)

BlockKeyDescription
serverhost / port / trustedProxiesListen (default 0.0.0.0:11032) and trusted proxies
databasepathSQLite (RBAC, endpoints, tokens, auditing)
authjwtSecret / jwtExpiryMin / cookieName / tokenPepper / secureCookiesConsole session and token hashing
naturalnaturalBin / sagenv / bp / natparm / fuserDbid / fuserFnr / fuserPath / numSessions / maxSessions / sessionIdleSecDefault environment
defaultEnvironment<DEV/TST/PRD>Environment used when an endpoint does not name one
environments<DEV/TST/PRD>Per-environment overrides (inherit from natural)
execdefaultTimeoutSec / maxTimeoutSec / maxConcurrent / queueTimeoutSec / ratePerMin / callRetentionDays / maxBodyBytes / maxOutputBytesExecution limits
correomailer, host, port, …E-mail delivery (password reset, token notices)
loggingdir / maxSizeMB / maxAgeDays / maxBackups / compressLog rotation
licensetrialDaysTrial days when there is no license.key
apiDocsaccessSpec visibility: public / private / off
updatesurlReserved: stored and editable, but no component consumes it today

Everything can also be edited from the console (Configuration, admin only). Applied immediately: timeouts, concurrency, queue, environments and apiDocs.access. A restart is required for server.host, server.port, database.path, auth.cookieName, logging.dir and auth.jwtSecret (the page reports which). The rate limit (exec.ratePerMin) is read at start-up, so a change only takes effect after the next restart.

Security

  • Per-endpoint Bearer tokens with scope, expiry, revocation and IP allowlist (--cidr), plus per-IP and per-token rate limiting.
  • Console with JWT, bcrypt, TOTP 2FA and RBAC; refresh-token rotation with reuse detection.
  • Roles and groups: permissions are granted to roles, groups attach a role and users belong to groups (permissions = union). Catalog: dashboard, endpoints, tokens, calls, sessions (each view/manage), users, config, logs. Roles & Groups page; the system group Administrators holds every permission and init puts the admin user in it.
  • Password recovery: in Users, Reset link generates a single-use link (60 min) and e-mails it; if mail is not configured or delivery fails, the console shows the reason and copies the link for manual delivery.
  • Strict validation of every value reaching Natural or the FUSER; no shell with user data; no path traversal.
  • Auditing of every call (metadata only, never bodies); configurable retention.

Troubleshooting

SymptomLikely cause
502 protocol: missing OUT sentinelStub not deployed or the program does not return control (STOP, loop)
502 natural execution failedNatural error; check the call log
Session: session boot timeoutMissing NSRPDRV driver or library/fuserPath/bp unresolved
Session: session error (RC=935)DEFINE DATA PARAMETER signature does not follow the convention (3 parameters)
503 server too busy / no free Natural sessionConcurrency or pool saturated: raise maxConcurrent/numSessions
504 timeoutProgram takes longer than timeout_sec
502 invalid json from programThe program emits single quotes: use H'22' (or GENERATE JSON) for JSON double quotes
400 missing param XRequired field without a value (query on GET/DELETE, body on POST/PUT); check path captures are not duplicated in fields_json
400 params too long (max 240 chars)Too many fields or values too long for one line

Limits (framing v2)

Input: one params line (≤240 characters per INPUT). Output: long replies (>240) are transmitted framed (v2: NSNAT:OUT:<len> + NSNAT:C:<len>:<chunk>×N + NSNAT:END RC=<n>), so the 240 WRITE per-line cap does not apply to the full payload; the real output cap is exec.maxOutputBytes (1 MB). Per-endpoint timeout (default 60 s, cap 300 s), configurable concurrency and per-token rate limit.

Quick reference

CommandDescription
nsnatapi init [dir]Create configuration, database and admin user
nsnatapi start|stop|status [dir]Service lifecycle
nsnatapi token [dir] --name X --endpoint Y --days N [--cidr ...]Create an API token (endpoint Y must exist)
nsnatapi token [dir] --list | --revoke <prefix>List or revoke tokens
nsnatapi passwd [dir] <user> [password]Reset a console password (generated when omitted)
nsnatapi license [dir]Show the license status (exit code 1 if invalid)
nsnatapi --helpHelp and global flags

Aussi disponible en : es