Guía de integración de la API de chat de AeroNyx

14 min de lectura

Envía y recibe mensajes de AeroNyx cifrados de extremo a extremo a través de HTTPS: firma de solicitudes, un inicio rápido ejecutable, el formato del sobre sellado, códigos de error y vectores de prueba.

La API de chat de AeroNyx permite que un servidor, un bot o una aplicación envíe y reciba mensajes individuales cifrados de extremo a extremo con usuarios de AeroNyx a través de HTTPS convencional. No utiliza la red de privacidad de AeroNyx, la VPN, el enrutamiento cebolla ni el buzón anónimo.

El envío es inmediato: si la App del destinatario está conectada, el mensaje le llega al instante. La recepción a través de esta API funciona por sondeo: GET /inbox/ devuelve lo que esté esperando y nunca mantiene la solicitud abierta. Para recibir en tiempo real, conecta la misma identidad al WebSocket de Chat Relay (wss://api.aeronyx.network/ws/relay/). Consulta la sección «Entrega en tiempo real» más abajo.

URL base: https://api.aeronyx.network/api/relay/developer/v1

Cómo funciona

  1. Crea una identidad. Un par de claves Ed25519 es tu identidad de chat. La clave pública de 32 bytes, escrita como 64 caracteres hexadecimales en minúsculas, es tu dirección de chat. No se necesita ninguna cuenta de AeroNyx.
  2. Firma cada solicitud. Cada llamada incluye un encabezado Authorization firmado con tu clave (Autenticación).
  3. Sella cada mensaje. Cifra y firma el mensaje por tu cuenta y envía solo el sobre resultante (Sobre de mensaje).
  4. Envía con POST /messages/.
  5. Recibe con GET /inbox/, descifra y almacena cada mensaje y luego confírmalo con POST /acks/.
EndpointAutenticaciónFinalidad
GET /capabilities/ningunaLímites del servicio y funciones compatibles
POST /messages/obligatoriaEnviar un mensaje sellado
GET /inbox/obligatoriaObtener los mensajes sin confirmar
POST /acks/obligatoriaConfirmar los mensajes que has almacenado

Inicio rápido

Este script envía un mensaje de una identidad nueva a otra, lo lee de vuelta, lo descifra y lo confirma. Guarda la implementación de referencia que aparece más abajo como aeronyx_chat.py, instala las dependencias con pip install pynacl requests y ejecútalo.

python
from __future__ import annotations

import hashlib, json, os, time, uuid

import requests
from nacl.signing import SigningKey

from aeronyx_chat import open_, seal

BASE = "https://api.aeronyx.network"
PREFIX = "/api/relay/developer/v1"


def call(key: SigningKey, method: str, path: str, body: dict | None = None):
    """Signs and sends one request. `path` is the full path after the host."""
    raw = b"" if body is None else json.dumps(body, separators=(",", ":")).encode()
    seconds = str(int(time.time()))
    nonce = os.urandom(16)
    digest = hashlib.sha256(
        b"AeroNyx/DeveloperChatRequestV1\0" + method.encode() + b"\0"
        + path.encode() + b"\0" + seconds.encode() + b"\0"
        + nonce + hashlib.sha256(raw).digest()
    ).digest()
    signature = key.sign(digest).signature.hex()
    pub = key.verify_key.encode().hex()
    headers = {
        "Authorization": f"AeroNyx-Chat-V1 {pub}:{seconds}:{nonce.hex()}:{signature}",
        "Content-Type": "application/json",
    }
    # Send exactly the bytes that were signed.
    return requests.request(method, BASE + path, data=raw or None,
                            headers=headers, timeout=20)


alice, bob = SigningKey.generate(), SigningKey.generate()
bob_address = bob.verify_key.encode().hex()

# 1. Alice seals and sends a message to Bob.
message_id = uuid.uuid4()
now = int(time.time())
sent = call(alice, "POST", PREFIX + "/messages/", {
    "receiver_pubkey": bob_address,
    "message_id": str(message_id),
    "discriminant": 11,
    "sent_at": now,
    "payload_b64": seal(alice, bob.verify_key.encode(), "Hello Bob", message_id, now),
})
print("send:", sent.status_code, sent.json())

# 2. Bob fetches his inbox, decrypts, and then acknowledges.
inbox = call(bob, "GET", PREFIX + "/inbox/?limit=50").json()
for item in inbox["messages"]:
    opened = open_(bob, item["payload_b64"])
    print("received:", opened["plaintext"], "from", opened["sender"][:8])
    # Store the message durably here, then acknowledge it.
    ack = call(bob, "POST", PREFIX + "/acks/", {"message_ids": [item["msg_id"]]})
    print("ack:", ack.status_code, ack.json())

Autenticación

Todos los endpoints, excepto GET /capabilities/, requieren este encabezado:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex es tu dirección de chat de 64 caracteres.
  • unix-seconds debe estar dentro de los 60 segundos del reloj del servidor.
  • nonce-hex son 16 bytes aleatorios escritos como 32 caracteres hexadecimales en minúsculas. Cada nonce puede usarse una sola vez.
  • signature-hex es una firma Ed25519, escrita como 128 caracteres hexadecimales, sobre este resumen (digest) de 32 bytes:
text
SHA256(
  "AeroNyx/DeveloperChatRequestV1\0" ||
  UPPERCASE_HTTP_METHOD || "\0" ||
  PATH_WITH_QUERY || "\0" ||
  ASCII_UNIX_SECONDS || "\0" ||
  NONCE_BYTES ||
  SHA256(EXACT_HTTP_BODY_BYTES)
)

PATH_WITH_QUERY es la ruta completa de la solicitud tal como se envía, incluidos el prefijo /api/relay/developer/v1 y cualquier cadena de consulta. Por ejemplo:

SolicitudPATH_WITH_QUERY que se firma
POST https://api.aeronyx.network/api/relay/developer/v1/messages//api/relay/developer/v1/messages/
GET https://api.aeronyx.network/api/relay/developer/v1/inbox/?limit=50/api/relay/developer/v1/inbox/?limit=50

EXACT_HTTP_BODY_BYTES son los bytes del cuerpo de la solicitud exactamente como se envían. Por ello, los espacios en blanco del JSON y el orden de las claves importan: serializa una vez, firma esos bytes y envía esos mismos bytes. Una solicitud sin cuerpo firma el SHA-256 de la cadena vacía.

Vector de prueba de autenticación

Úsalo para comprobar tu código de firma antes de llamar a la API. La clave es solo para pruebas; nunca la uses para mensajes reales.

json
{
  "seed_hex": "0303030303030303030303030303030303030303030303030303030303030303",
  "public_key_hex": "ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1",
  "method": "POST",
  "path_with_query": "/api/relay/developer/v1/messages/",
  "unix_seconds": "1780000000",
  "nonce_hex": "000102030405060708090a0b0c0d0e0f",
  "body": "{\"receiver_pubkey\":\"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394\",\"message_id\":\"018f5164-c1b2-7e2b-9d8a-2c60597f4c2f\",\"discriminant\":11,\"sent_at\":1780000000,\"payload_b64\":\"AAAA\"}",
  "body_sha256_hex": "1971a2ef86b2a085e033fd65313981c8668204d49cedbe530875036d1f45c9cc",
  "digest_hex": "6f4d0ca6dcd1221780b2dae45dd94e4bae7e79f266e8bb69f2d50c20de08b276",
  "signature_hex": "14ed2108e1ecf8ce5d04299e59af6d00dec218d62592de127e6a640f235ea4e9703004561fc08e929d0159f92fc51d35a881eec7d0eb16f19d94270c6262cb0c",
  "authorization": "AeroNyx-Chat-V1 ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1:1780000000:000102030405060708090a0b0c0d0e0f:14ed2108e1ecf8ce5d04299e59af6d00dec218d62592de127e6a640f235ea4e9703004561fc08e929d0159f92fc51d35a881eec7d0eb16f19d94270c6262cb0c"
}

Las firmas Ed25519 son deterministas, por lo que las mismas entradas deben producir exactamente este signature_hex. Si tu body_sha256_hex es distinto, los bytes de tu cuerpo son distintos.

Solución de problemas del 401

La API responde con el mismo 401 ante cualquier fallo de autenticación, así que revisa estos puntos en orden:

  1. El reloj: unix-seconds está dentro de los 60 segundos de la hora real.
  2. El nonce es nuevo en cada solicitud, de 16 bytes, escrito como 32 caracteres hexadecimales en minúsculas.
  3. La ruta firmada incluye /api/relay/developer/v1 y la cadena de consulta.
  4. El método firmado está en mayúsculas y coincide con la solicitud.
  5. Los bytes del cuerpo firmado son los bytes que envías. No vuelvas a serializar el JSON después de firmar.
  6. Tu digest y tu firma coinciden con el vector de prueba anterior.

Endpoints

Información del servicio

GET /capabilities/ no necesita autenticación y devuelve los límites actuales y las funciones compatibles.

Enviar un mensaje

POST /messages/

json
{
  "receiver_pubkey": "<64 lowercase hex characters>",
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "discriminant": 11,
  "sent_at": 1780000000,
  "payload_b64": "<sealed envelope, Base64>"
}

Los cinco campos son obligatorios y no se permite ningún otro.

  • receiver_pubkey es la dirección de chat del destinatario. Debe ser distinta de la tuya.
  • message_id es un UUID. Usa un UUID aleatorio nuevo para cada mensaje y conserva el cuerpo exacto para los reintentos. Enviar el mismo message_id con campos distintos devuelve 409 idempotency_conflict.
  • discriminant es 11 para un mensaje individual que debe ver un usuario de AeroNyx App.
  • sent_at son segundos Unix y debe estar dentro de los 300 segundos de la hora del servidor.
  • payload_b64 es el sobre sellado descrito en Sobre de mensaje.

El relay solo comprueba que payload_b64 sea Base64 válido de entre 1 byte y 191 KiB. No puede distinguir un sobre de otros datos, así que una carga que no sea un sobre válido se acepta aquí y la App del destinatario la descarta en silencio. Prueba tu sobre con el vector de referencia.

Una solicitud correcta devuelve HTTP 202:

json
{
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "accepted": true,
  "delivered_live": false
}

accepted significa que el relay almacenó el texto cifrado de forma duradera. delivered_live es true cuando el relay entregó el mensaje a la entrega en vivo sin errores. También es true aunque el destinatario no tenga ninguna conexión abierta, así que no indica si alguien está en línea. Ninguno de los dos campos significa que el destinatario haya descifrado o mostrado el mensaje; eso lo indica un acuse de entrega en tu bandeja de entrada (consulta «Eventos en la bandeja de entrada»).

Obtener la bandeja de entrada

GET /inbox/?limit=50

Devuelve hasta 100 eventos sin confirmar. El mismo evento puede aparecer de nuevo tras un tiempo de espera agotado o un reinicio.

json
{
  "messages": [{
    "sender_pubkey": "<hex>",
    "discriminant": 11,
    "payload_b64": "<ciphertext>",
    "timestamp": 1780000000,
    "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"
  }],
  "has_more": false
}

Si has_more es true, confirma lo que has almacenado y vuelve a obtener la bandeja.

Eventos en la bandeja de entrada

La bandeja de entrada contiene todo lo que el relay ha puesto en cola para tu identidad, no solo mensajes nuevos. Un elemento sin control_type es un mensaje: abre payload_b64 como se describe en «Sobre de mensaje». Un elemento con control_type es un evento sobre un mensaje. Comprueba control_type antes de descifrar nada: los acuses no tienen payload_b64.

control_typeSignificadomsg_id
message_receiptLa App del destinatario descifró y guardó un mensaje que enviaste.El message_id de tu mensaje
message_readEl destinatario leyó tu mensaje. Solo se envía si tiene activados los acuses de lectura.El message_id de tu mensaje
message_editEl remitente editó un mensaje (target_msg_id, el mismo ID).El ID del mensaje editado
message_revokeEl remitente retiró un mensaje.El ID del mensaje retirado
message_reactionSe añadió o quitó una reacción a target_msg_id.El ID de la reacción

Las ediciones, retiradas y reacciones van firmadas (payload_sig); sus formatos se describen en Integración de clientes con AeroNyx Chat Relay. Confirmar un ID elimina todos los elementos en cola que lo llevan, así que procesa una página completa antes de confirmar. Confirma también los tipos de evento que no manejes; si no, se devolverán de nuevo.

Confirmar mensajes

POST /acks/

json
{
  "message_ids": ["018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"]
}

Confirma un mensaje solo después de haberlo validado, descifrado y escrito en almacenamiento duradero. Los ID desconocidos se omiten de la lista acknowledged de la respuesta.

Modelo de entrega

  • La entrega es al menos una vez. Elimina duplicados por control_type junto con msg_id: un acuse lleva el ID del mensaje que confirma.
  • El texto cifrado no entregado se conserva durante 72 horas. No trates el servicio como un historial permanente.
  • Reintenta un envío fallido con exactamente el mismo cuerpo y un nonce de autenticación nuevo.

Entrega en tiempo real

Envío

POST /messages/ entrega tu mensaje a todas las conexiones que el destinatario tenga abiertas y, al mismo tiempo, lo mantiene en cola hasta que la App del destinatario lo confirme. Un destinatario con la App conectada lo recibe al instante; uno sin conexión lo recibe la próxima vez que la App se conecte, dentro de 72 horas.

Los mensajes enviados a través de esta API no generan una notificación push. Un destinatario con la App cerrada no recibe aviso y verá el mensaje la próxima vez que abra la App.

Recepción por sondeo

GET /inbox/ responde al instante, haya o no algo esperando. Un mensaje te llega solo en tu siguiente llamada, así que el retraso que ves es tu intervalo de sondeo. Para conversaciones, recibe por el WebSocket.

Recepción en tiempo real

Conecta la misma identidad al WebSocket de Chat Relay. No se necesita cuenta ni registro.

  1. Abre wss://api.aeronyx.network/ws/relay/ e inicia sesión con la misma clave Ed25519, como se describe en el apartado de inicio de sesión de Integración de clientes con AeroNyx Chat Relay.
  2. Tras iniciar sesión, el relay reenvía primero todo lo que siga en cola para ti (from_offline es true) y después envía cada elemento nuevo en cuanto llega. Los mensajes llegan como tramas relay_envelope con los mismos campos que un mensaje de la bandeja de entrada; los acuses, ediciones, retiradas y reacciones llegan como tramas cuyo type es el control_type de la tabla anterior.
  3. Cuando hayas guardado un elemento, confírmalo:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

El WebSocket y GET /inbox/ leen la misma cola: un elemento confirmado en uno desaparece del otro, y POST /acks/ sirve para ambos. Puedes seguir enviando por HTTPS mientras recibes por el WebSocket.

Límites y errores

LímiteValor
Carga sellada191 KiB (la solicitud JSON con Base64 queda por debajo del límite de la solicitud)
Cuerpo de la solicitud256 KiB
Página de la bandeja de entrada y lote de confirmaciones100 elementos
Retención sin conexión72 horas
EstadoSignificado
400 invalid_jsonEl cuerpo no es un objeto JSON.
400 invalid_requestEl cuerpo no contiene exactamente los cinco campos, receiver_pubkey no son 64 caracteres hexadecimales en minúsculas o es igual a tu propia dirección, discriminant está fuera del rango 1-255, message_id no es un UUID, o payload_b64 no es Base64 válido.
400 invalid_payloadLa carga decodificada está vacía o supera los 191 KiB.
400 message_expiredsent_at difiere de la hora del servidor en más de 300 segundos.
401La autenticación falta, ha caducado, no es válida o es una repetición. Consulta Solución de problemas del 401.
403La política del destinatario rechazó a este remitente.
409Conflicto de idempotencia, o una solicitud idéntica sigue en curso.
429Se alcanzó el límite de velocidad del remitente: 50 solicitudes por segundo y 100.000 por día UTC para cada identidad. Espera el tiempo indicado en el encabezado Retry-After.
503La entrega duradera o la autenticación no está disponible temporalmente. Reintenta con el cuerpo exacto y un nonce nuevo.

Lo que esta guía no cubre

Esta guía cubre los mensajes uno a uno por HTTPS. Para todo lo demás, conecta la misma identidad al WebSocket del relé de chat y consulta Integración de clientes con AeroNyx Chat Relay:

  • Mensajes de grupo, presencia e indicadores de escritura.
  • Archivos adjuntos cifrados, incluidos los mensajes de voz y los archivos de vídeo.
  • Llamadas de voz y vídeo. Los marcos de señalización figuran en ese artículo, pero todavía no se ha publicado una guía completa de integración de llamadas (token de medios, firma de invitación y cifrado de la llamada).

Privacidad y visibilidad

El contenido de los mensajes está cifrado de extremo a extremo: se sella en tu lado con una clave derivada de las identidades del remitente y del destinatario, que AeroNyx nunca posee, y la API almacena y reenvía únicamente el texto cifrado. Para entregar los mensajes, el servicio puede ver las claves públicas del remitente y del destinatario, la hora del mensaje, el tamaño, el estado de entrega y la dirección IP de cada solicitud. HTTPS no oculta estos metadatos al servicio.

Sobre de mensaje

Los mensajes que debe ver un usuario de AeroNyx App usan discriminant: 11 y un sobre sellado de extremo a extremo en payload_b64. El relay nunca ve la clave ni el texto en claro. Todos los enteros son little-endian.

1. Clave compartida

Ambas partes derivan la misma clave de 32 bytes a partir de sus identidades de chat Ed25519:

text
my_x25519_secret = clamp(SHA-512(my_ed25519_seed)[0..32])
peer_x25519      = Edwards-to-Montgomery(peer_ed25519_public_key)
raw              = X25519(my_x25519_secret, peer_x25519)
key              = HKDF-SHA256(salt = empty, ikm = raw, info = "AERONYX-P2P-KEY", length = 32)

Con libsodium esto es crypto_sign_ed25519_sk_to_curve25519, crypto_sign_ed25519_pk_to_curve25519 y crypto_scalarmult, seguidos de HKDF. Los remitentes siempre usan la clave HKDF. Las versiones muy antiguas de la App enviaban con raw directamente, así que un receptor puede probar raw si la clave HKDF no logra descifrar.

2. Texto en claro

Un mensaje de texto es su texto UTF-8. Los mensajes de la App que incluyen adjuntos, respuestas o reenvíos son un objeto JSON con "type": "aeronyx_message" y un campo text; muestra text si no admites el resto.

3. Cifrado

XChaCha20-Poly1305 con un nonce aleatorio nuevo de 24 bytes y sin datos asociados. ciphertext, más abajo, es la salida del cifrado, incluida su etiqueta de 16 bytes.

4. Firma

El remitente firma estos 121 bytes con Ed25519:

text
sender_pubkey   32 bytes
message_id      16 bytes   (the raw bytes of the UUID sent as message_id)
receiver_pubkey 32 bytes
timestamp        8 bytes   (u64, Unix seconds; use the same value as sent_at)
content_type     1 byte    (0 = text)
SHA-256(ciphertext) 32 bytes

5. Sobre

text
message_id      16 bytes
sender_pubkey   32 bytes
receiver_pubkey 32 bytes
timestamp        8 bytes   (u64)
ciphertext_len   8 bytes   (u64)
ciphertext       ciphertext_len bytes
nonce           24 bytes
content_type     4 bytes   (u32, 0 = text)
signature       64 bytes

payload_b64 es el Base64 estándar de estos bytes. Ten en cuenta que content_type ocupa 1 byte dentro de los datos firmados, pero 4 bytes en el sobre.

Recepción

Analiza el sobre, comprueba que receiver_pubkey sea tu identidad, verifica la firma con el sender_pubkey del sobre y luego descifra con la clave derivada de ese remitente. Descarta el mensaje si falla cualquier paso. La App descarta los mensajes en vivo cuyo timestamp difiere en más de 300 segundos de su reloj.

Implementación de referencia (Python)

python
"""AeroNyx 1:1 chat envelope (discriminant 11). Requires PyNaCl."""

from __future__ import annotations

import base64
import hashlib
import hmac
import os
import struct
import uuid

from nacl.bindings import (
    crypto_aead_xchacha20poly1305_ietf_decrypt,
    crypto_aead_xchacha20poly1305_ietf_encrypt,
    crypto_scalarmult,
    crypto_sign_ed25519_pk_to_curve25519,
    crypto_sign_ed25519_sk_to_curve25519,
)
from nacl.signing import SigningKey, VerifyKey

CONTENT_TEXT = 0
_HKDF_INFO = b"AERONYX-P2P-KEY"


def _hkdf_sha256(ikm: bytes, info: bytes, length: int = 32) -> bytes:
    # RFC 5869 with an empty salt (= 32 zero bytes).
    prk = hmac.new(b"\x00" * 32, ikm, hashlib.sha256).digest()
    okm, block, counter = b"", b"", 1
    while len(okm) < length:
        block = hmac.new(prk, block + info + bytes([counter]), hashlib.sha256).digest()
        okm += block
        counter += 1
    return okm[:length]


def chat_key(my_key: SigningKey, peer_public: bytes, hkdf: bool = True) -> bytes:
    """X25519 over the Ed25519 identities, then HKDF-SHA256."""
    my_secret = bytes(my_key) + my_key.verify_key.encode()  # libsodium 64-byte sk
    raw = crypto_scalarmult(
        crypto_sign_ed25519_sk_to_curve25519(my_secret),
        crypto_sign_ed25519_pk_to_curve25519(peer_public),
    )
    return _hkdf_sha256(raw, _HKDF_INFO) if hkdf else raw


def _sign_data(sender, message_id, receiver, timestamp, content_type, ciphertext):
    return (
        sender
        + message_id
        + receiver
        + struct.pack("<Q", timestamp)
        + bytes([content_type])  # one byte here ...
        + hashlib.sha256(ciphertext).digest()
    )


def seal(my_key: SigningKey, receiver: bytes, text: str, message_id: uuid.UUID,
         timestamp: int, nonce: bytes | None = None) -> str:
    """Returns payload_b64 for POST /messages/ (discriminant 11)."""
    nonce = nonce if nonce is not None else os.urandom(24)
    key = chat_key(my_key, receiver)
    ciphertext = crypto_aead_xchacha20poly1305_ietf_encrypt(
        text.encode("utf-8"), None, nonce, key)  # ciphertext || 16-byte tag, no AAD
    sender = my_key.verify_key.encode()
    mid = message_id.bytes
    signature = my_key.sign(
        _sign_data(sender, mid, receiver, timestamp, CONTENT_TEXT, ciphertext)
    ).signature
    envelope = (
        mid
        + sender
        + receiver
        + struct.pack("<Q", timestamp)
        + struct.pack("<Q", len(ciphertext))
        + ciphertext
        + nonce
        + struct.pack("<I", CONTENT_TEXT)  # ... four bytes here
        + signature
    )
    return base64.b64encode(envelope).decode("ascii")


def open_(my_key: SigningKey, payload_b64: str) -> dict:
    """Verifies and decrypts an inbox payload addressed to my_key."""
    env = base64.b64decode(payload_b64, validate=True)
    if len(env) < 16 + 32 + 32 + 8 + 8 + 16 + 24 + 4 + 64:
        raise ValueError("envelope too short")
    mid, sender, receiver = env[0:16], env[16:48], env[48:80]
    timestamp, ct_len = struct.unpack("<QQ", env[80:96])
    if len(env) != 96 + ct_len + 24 + 4 + 64:
        raise ValueError("envelope length mismatch")
    ciphertext = env[96:96 + ct_len]
    nonce = env[96 + ct_len:120 + ct_len]
    (content_type,) = struct.unpack("<I", env[120 + ct_len:124 + ct_len])
    signature = env[124 + ct_len:]
    if receiver != my_key.verify_key.encode():
        raise ValueError("envelope is addressed to another identity")
    VerifyKey(sender).verify(
        _sign_data(sender, mid, receiver, timestamp, content_type, ciphertext), signature)
    # Current App versions use the HKDF key; very old ones sent with the raw key.
    for hkdf in (True, False):
        try:
            plaintext = crypto_aead_xchacha20poly1305_ietf_decrypt(
                ciphertext, None, nonce, chat_key(my_key, sender, hkdf))
            break
        except Exception:  # noqa: BLE001 - try the legacy key next
            plaintext = None
    if plaintext is None:
        raise ValueError("decryption failed")
    return {
        "message_id": str(uuid.UUID(bytes=mid)),
        "sender": sender.hex(),
        "timestamp": timestamp,
        "content_type": content_type,
        "plaintext": plaintext.decode("utf-8"),
    }

Vector de referencia

Claves, nonce, ID y hora fijos, solo para pruebas. Nunca uses estas claves.

json
{
  "sender_seed_hex": "0101010101010101010101010101010101010101010101010101010101010101",
  "sender_pubkey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
  "receiver_seed_hex": "0202020202020202020202020202020202020202020202020202020202020202",
  "receiver_pubkey": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394",
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "timestamp": 1780000000,
  "nonce_hex": "000102030405060708090a0b0c0d0e0f1011121314151617",
  "plaintext": "Hello from AeroNyx",
  "shared_key_hex": "dd08f8bb45a43c9bc186fdaf436d47e8e05b0aca44fa2bb9473769da0407652f",
  "payload_b64": "AY9RZMGyfiudiixgWX9ML4qI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29cgTl3Dqh9F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QApRhqAAAAACIAAAAAAAAA1vVyzdTx1kAdPEI7AGO12ppwLwaME0K9PJKd+u7OgWq0rwABAgMEBQYHCAkKCwwNDg8QERITFBUWFwAAAACAlpInhB5A9cD4LqKAT0OE0OpbMqT5w/xB8IiHrzuyg9LPjF9gDHdOekPNoHNxDMeu3Ift0UB69OGSofdfwKIN"
}

seal(alice, bob_public, "Hello from AeroNyx", message_id, 1780000000, nonce=bytes(range(24))) debe producir exactamente este payload_b64, y open_(bob, payload_b64) debe devolver el texto en claro. Este vector está verificado con el acuerdo de claves, el firmante y el cifrador de la propia AeroNyx App, y con el decodificador estricto de sobres del nodo.

<!-- faq:start -->

Preguntas frecuentes

¿Qué es la API de chat de AeroNyx?

Es una API HTTPS con versiones para intercambiar mensajes individuales cifrados de extremo a extremo con usuarios de AeroNyx desde un servidor, un bot o una aplicación. Tiene cuatro endpoints: capacidades, envío, bandeja de entrada y confirmación. Transporta únicamente sobres sellados; el contenido se cifra en tu lado.

¿Necesito una cuenta de AeroNyx para usar la API?

No. Un par de claves Ed25519 es la identidad. Genéralo localmente y firma cada solicitud con él; la clave pública es tu dirección de chat.

¿Firmo /messages/ o la ruta completa?

La ruta completa tal como se envía, empezando por /api/relay/developer/v1, más la cadena de consulta si la hay. Firmar solo /messages/ produce un 401. El vector de prueba de autenticación de esta página muestra las entradas exactas.

¿Por qué recibo 401?

Todos los fallos de autenticación devuelven el mismo 401. Las causas habituales son un reloj desfasado en más de 60 segundos, un nonce reutilizado, una ruta o un cuerpo firmados que difieren de lo enviado, o un método HTTP en minúsculas en los datos firmados. Repasa la lista de comprobación de Solución de problemas del 401 y compara tu digest con el vector de prueba.

¿Por qué la API devuelve 202 pero el mensaje nunca aparece en AeroNyx App?

El relay acepta cualquier carga Base64 válida, pero la App solo muestra un sobre sellado que pueda verificar y descifrar. Comprueba tu payload_b64 con el vector de referencia de esta página, asegúrate de que el receiver_pubkey del sobre sea el destinatario y mantén la marca de tiempo del sobre dentro de los 300 segundos de la hora actual. Enviar texto plano codificado en Base64 se acepta, pero nunca se muestra.

¿Por qué la API devuelve invalid_request?

Las causas más comunes son enviar a tu propia clave pública, un cuerpo que no contiene exactamente los cinco campos documentados, un message_id que no es un UUID o un discriminant fuera del rango 1-255. Consulta la tabla de errores en Límites y errores.

¿Puede AeroNyx leer los mensajes enviados a través de esta API?

No. Los sobres se cifran con una clave derivada de las identidades del remitente y del destinatario, que AeroNyx nunca posee. El servicio puede ver metadatos de entrega como las claves públicas, las horas, los tamaños y las direcciones IP.

¿Cuánto tiempo se conservan los mensajes no entregados?

Hasta 72 horas. Confirma cada mensaje solo después de haberlo descifrado y almacenado; los mensajes sin confirmar pueden entregarse de nuevo.

¿Qué lenguajes de programación puedo usar?

Cualquier lenguaje con Ed25519, X25519, HKDF-SHA256 y XChaCha20-Poly1305, lo que incluye los bindings de libsodium para la mayoría de los lenguajes. Esta página incluye una implementación de referencia completa en Python y un inicio rápido ejecutable.

¿La API de chat de AeroNyx es en tiempo real?

El envío sí: un destinatario con la App conectada recibe tu mensaje al instante. La recepción por HTTPS es por sondeo, así que verás los mensajes nuevos en tu siguiente llamada a GET /inbox/. Para recibir en tiempo real, conecta la misma identidad al WebSocket de Chat Relay en wss://api.aeronyx.network/ws/relay/; lee la misma cola.

¿Un usuario de AeroNyx recibe una notificación por un mensaje que envío?

Por ahora no. Los mensajes enviados a través de esta API no generan una notificación push, así que un usuario con la App cerrada verá tu mensaje la próxima vez que la abra. Un usuario con la App conectada lo ve al instante.

<!-- faq:end -->