AeroNyx 聊天 API 集成指南

11 分钟阅读

通过 HTTPS 收发端到端加密的 AeroNyx 消息:请求签名、可运行的快速开始、已封装的信封格式、错误码和测试向量。

AeroNyx 聊天 API 让服务器、机器人或应用程序可以通过普通 HTTPS 与 AeroNyx 用户收发端到端加密的一对一消息。它不使用 AeroNyx 隐私网络、VPN、洋葱路由或匿名邮箱。

发送是实时的:接收方的 App 在线时,消息会立刻送达。通过此 API 接收则是轮询:GET /inbox/ 只返回当前正在等待的内容,不会保持请求不断开。如需实时接收,请让同一个身份连接到聊天中继 WebSocket(wss://api.aeronyx.network/ws/relay/),详见下方“实时投递”一节。

基础 URL:https://api.aeronyx.network/api/relay/developer/v1

工作原理

  1. 创建身份。 一对 Ed25519 密钥就是你的聊天身份。32 字节的公钥以 64 个小写十六进制字符表示,即为你的聊天地址。无需 AeroNyx 账户。
  2. 为每个请求签名。 每次调用都携带用你的密钥签名的 Authorization 请求头(认证)。
  3. 封装每条消息。 在你这一侧加密并签名消息,只发送生成的信封(消息信封)。
  4. 发送: 使用 POST /messages/ 发送。
  5. 接收: 使用 GET /inbox/ 获取,解密并保存每条消息,然后使用 POST /acks/ 确认。
端点认证用途
GET /capabilities/无服务限制和支持的功能
POST /messages/必需发送已封装的消息
GET /inbox/必需获取未确认的消息
POST /acks/必需确认你已保存的消息

快速开始

此脚本会从一个新身份向另一个新身份发送消息,再将其读回、解密并确认。请将下方的参考实现保存为 aeronyx_chat.py,使用 pip install pynacl requests 安装依赖,然后运行。

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())

认证

除 GET /capabilities/ 外,每个端点都需要此请求头:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex 是你的 64 字符聊天地址。
  • unix-seconds 必须与服务器时钟相差不超过 60 秒。
  • nonce-hex 是 16 个随机字节,以 32 个小写十六进制字符表示。每个 nonce 只能使用一次。
  • signature-hex 是对下面这个 32 字节摘要的 Ed25519 签名,以 128 个十六进制字符表示:
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 是实际发送的完整请求路径,包括 /api/relay/developer/v1 前缀和任何查询字符串。 例如:

请求需要签名的 PATH_WITH_QUERY
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 是实际发送的请求体字节。因此 JSON 的空白和键顺序都很重要:只序列化一次,对这些字节签名,并发送同样的这些字节。没有请求体的请求,签名的是空字符串的 SHA-256。

认证测试向量

在调用 API 之前,用它来检查你的签名代码。该密钥仅用于测试,切勿用于真实消息。

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"
}

Ed25519 签名是确定性的,因此相同的输入必须得到完全一致的 signature_hex。如果你的 body_sha256_hex 不同,说明你的请求体字节不同。

排查 401

对于所有认证失败,API 都返回同样的 401,因此请按顺序检查:

  1. 时钟:unix-seconds 与真实时间相差不超过 60 秒。
  2. 每个请求的 nonce 都是新的,长 16 字节,以 32 个小写十六进制字符表示。
  3. 签名的路径包含 /api/relay/developer/v1 和查询字符串。
  4. 签名的方法为大写,并与请求一致。
  5. 签名的请求体字节就是你发送的字节。签名之后不要重新序列化 JSON。
  6. 你的摘要和签名与上面的测试向量一致。

端点

服务信息

GET /capabilities/ 无需认证,返回当前的限制和支持的功能。

发送消息

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>"
}

五个字段全部必填,且不允许有其他字段。

  • receiver_pubkey 是接收方的聊天地址。它必须与你自己的地址不同。
  • message_id 是 UUID。每条消息使用一个新的随机 UUID,重试时保留完全相同的请求体。以相同的 message_id 但不同的字段发送,会返回 409 idempotency_conflict。
  • discriminant 为 11,表示 AeroNyx App 用户应当看到的一对一消息。
  • sent_at 是 Unix 秒数,必须与服务器时间相差不超过 300 秒。
  • payload_b64 是“消息信封”中描述的已封装信封。

中继只检查 payload_b64 是否为 1 字节到 191 KiB 的有效 Base64。它无法区分信封与其他数据,因此不是有效信封的负载在这里会被接受,随后被接收方的 App 静默丢弃。请对照标准向量测试你的信封。

请求成功时返回 HTTP 202:

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

accepted 表示中继已将密文持久化存储。delivered_live 为 true 只表示中继已把消息交给实时投递且没有出错;即使接收方没有任何连接,它也是 true,所以不能用来判断对方是否在线。两者都不代表接收方已经解密或显示了该消息;收件箱里的投递回执才代表这件事(见“收件箱中的事件”)。

获取收件箱

GET /inbox/?limit=50

最多返回 100 个未确认的事件。同一个事件在超时或重启之后可能再次出现。

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

如果 has_more 为 true,请确认你已保存的消息,然后再次获取。

收件箱中的事件

收件箱存放中继为你的身份排队的所有条目,不只是新消息。没有 control_type 的条目是消息:按“消息信封”一节打开 payload_b64。有 control_type 的条目是关于某条消息的事件。解密任何内容之前,请先检查 control_type:回执没有 payload_b64。

control_type含义msg_id
message_receipt接收方的 App 已解密并保存你发送的消息。你那条消息的 message_id
message_read接收方已读你的消息。只有对方开启已读回执时才会发送。你那条消息的 message_id
message_edit发送方编辑了一条消息(target_msg_id,与 msg_id 相同)。被编辑消息的 ID
message_revoke发送方撤回了一条消息。被撤回消息的 ID
message_reaction对 target_msg_id 添加或移除了表情回应。表情回应的 ID

编辑、撤回和表情回应都带签名(payload_sig),格式见 AeroNyx 聊天中继客户端集成。确认一个 ID 会移除队列中所有带该 ID 的条目,因此请处理完整页再确认。你不处理的事件类型也请确认,否则会再次返回。

确认消息

POST /acks/

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

只有在验证、解密并将消息写入持久化存储之后,才可以确认该消息。未知的 ID 不会出现在响应的 acknowledged 列表中。

投递模型

  • 投递至少一次。请按 control_type 加 msg_id 去重:回执携带的是它所确认的那条消息的 ID。
  • 未投递的密文保留 72 小时。不要把该服务当作永久的历史记录。
  • 重试失败的发送时,请使用完全相同的请求体和新的认证 nonce。

实时投递

发送

POST /messages/ 会把消息交给接收方当前打开的每一条连接,同时把它保留在队列中,直到接收方的 App 确认为止。接收方的 App 在线时会立刻收到;离线时,会在 App 下次连接时收到(72 小时内)。

通过此 API 发送的消息不会触发推送通知。App 已关闭的接收方不会收到提醒,会在下次打开 App 时看到消息。

通过轮询接收

GET /inbox/ 会立即返回,无论是否有内容在等待。消息要等你下一次调用才会拿到,所以你看到的延迟就是你的轮询间隔。对话类场景请改用 WebSocket 接收。

实时接收

让同一个身份连接到聊天中继 WebSocket,无需账号或注册。

  1. 打开 wss://api.aeronyx.network/ws/relay/,并用同一把 Ed25519 密钥登录,方法见 AeroNyx 聊天中继客户端集成 的“登录”一节。
  2. 登录后,中继会先重发所有仍在队列中的条目(from_offline 为 true),之后每个新条目一到就立刻发送。消息以 relay_envelope 帧送达,字段与收件箱中的消息相同;回执、编辑、撤回和表情回应则以 type 等于上表 control_type 的帧送达。
  3. 保存条目后,确认它:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

WebSocket 与 GET /inbox/ 读取的是同一个队列:在其中一边确认过的条目,另一边也不会再出现,POST /acks/ 对两者都有效。你可以继续用 HTTPS 发送,同时用 WebSocket 接收。

限制与错误

限制值
已封装的负载191 KiB(Base64 的 JSON 请求仍低于请求限制)
请求体256 KiB
收件箱每页和确认批次100 项
离线保留时间72 小时
状态含义
400 invalid_json请求体不是 JSON 对象。
400 invalid_request请求体不是恰好包含这五个字段,receiver_pubkey 不是 64 个小写十六进制字符或等于你自己的地址,discriminant 超出 1-255 范围,message_id 不是 UUID,或者 payload_b64 不是有效的 Base64。
400 invalid_payload解码后的负载为空或大于 191 KiB。
400 message_expiredsent_at 与服务器时间相差超过 300 秒。
401认证缺失、已过期、无效或被重放。参见“排查 401”。
403接收方的策略拒绝了该发送方。
409幂等性冲突,或相同的请求仍在处理中。
429达到发送方频率限制:每个身份每秒 50 个请求,每个 UTC 日 100,000 个请求。请等待 Retry-After 响应头中指定的时间。
503持久化投递或认证暂时不可用。请使用完全相同的请求体和新的 nonce 重试。

本指南不涵盖的内容

本指南只涵盖通过 HTTPS 收发一对一消息。其他功能请让同一个身份连接聊天中继 WebSocket,并参阅 AeroNyx 聊天中继客户端集成:

  • 群组消息、在线状态与输入中提示。
  • 加密附件,包括语音消息和视频文件。
  • 语音和视频通话。该文章列出了信令帧,但完整的通话接入指南(媒体令牌、邀请签名与通话加密)尚未发布。

隐私与可见性

消息内容是端到端加密的:它在你这一侧使用由发送方和接收方身份派生的密钥封装,AeroNyx 从不持有该密钥,API 只存储和转发密文。为了投递消息,服务可以看到发送方和接收方的公钥、消息时间、大小、投递状态以及每个请求的 IP 地址。HTTPS 不会对服务隐藏这些元数据。

消息信封

AeroNyx App 用户应当看到的消息使用 discriminant: 11,并在 payload_b64 中放入端到端封装的信封。中继从不接触密钥或明文。所有整数均为小端序。

1. 共享密钥

双方都从各自的 Ed25519 聊天身份派生出同一个 32 字节密钥:

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)

使用 libsodium 时,这对应 crypto_sign_ed25519_sk_to_curve25519、crypto_sign_ed25519_pk_to_curve25519 和 crypto_scalarmult,之后再执行 HKDF。发送方始终使用 HKDF 密钥。非常旧的 App 版本直接使用 raw 发送,因此如果 HKDF 密钥解密失败,接收方可以尝试 raw。

2. 明文

文本消息就是其 UTF-8 文本。来自 App 且带有附件、回复或转发的消息是一个 JSON 对象,包含 "type": "aeronyx_message" 和 text 字段;如果你不支持其余部分,请渲染 text。

3. 加密

使用 XChaCha20-Poly1305,配以全新的随机 24 字节 nonce,且不带关联数据。下文的 ciphertext 是包含 16 字节标签的密码输出。

4. 签名

发送方使用 Ed25519 对以下 121 字节签名:

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. 信封

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 是这些字节的标准 Base64。注意,content_type 在被签名的数据中是 1 字节,而在信封中是 4 字节。

接收

解析信封,检查 receiver_pubkey 是你的身份,对照信封中的 sender_pubkey 验证签名,然后使用由该发送方派生的密钥解密。任何一步失败都要丢弃该消息。App 会丢弃 timestamp 与其时钟相差超过 300 秒的实时消息。

参考实现(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"),
    }

标准向量

固定的密钥、nonce、ID 和时间,仅用于测试。切勿使用这些密钥。

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))) 必须生成与此完全一致的 payload_b64,并且 open_(bob, payload_b64) 必须返回该明文。此向量已对照 AeroNyx App 自身的密钥协商、签名器和密码实现,以及节点的严格信封解码器进行验证。

<!-- faq:start -->

常见问题

什么是 AeroNyx 聊天 API?

它是一个带版本的 HTTPS API,用于从服务器、机器人或应用程序与 AeroNyx 用户交换端到端加密的一对一消息。它有四个端点:capabilities、发送、收件箱和确认。它只承载已封装的信封;内容在你这一侧加密。

使用该 API 需要 AeroNyx 账户吗?

不需要。一对 Ed25519 密钥就是身份。在本地生成它,并用它为每个请求签名;公钥就是你的聊天地址。

我应该对 /messages/ 签名,还是对完整路径签名?

对实际发送的完整路径签名,以 /api/relay/developer/v1 开头,如果有查询字符串则一并包含。只对 /messages/ 签名会得到 401。本页的认证测试向量给出了确切的输入。

为什么我会收到 401?

所有认证失败都返回同样的 401。常见原因是时钟偏差超过 60 秒、重复使用 nonce、签名的路径或请求体与实际发送的不一致,或者签名数据中的 HTTP 方法是小写。请按“排查 401”下的清单逐项检查,并将你的摘要与测试向量对比。

为什么 API 返回 202,但消息始终没有出现在 AeroNyx App 中?

中继接受任何有效的 Base64 负载,但 App 只显示它能够验证并解密的已封装信封。请将你的 payload_b64 与本页的标准向量对照检查,确保信封的 receiver_pubkey 是接收方,并让信封的时间戳与当前时间相差不超过 300 秒。把纯文本编码为 Base64 后发送会被接受,但永远不会显示。

为什么 API 返回 invalid_request?

最常见的原因是发给你自己的公钥、请求体不是恰好包含文档规定的五个字段、message_id 不是 UUID,或者 discriminant 超出 1-255 范围。请参见“限制与错误”下的错误表。

AeroNyx 能读取通过此 API 发送的消息吗?

不能。信封使用由发送方和接收方身份派生的密钥加密,AeroNyx 从不持有该密钥。服务可以看到投递元数据,例如公钥、时间、大小和 IP 地址。

未投递的消息会保留多久?

最多 72 小时。只有在解密并保存每条消息之后才确认它;未确认的消息可能被再次投递。

我可以使用哪些编程语言?

任何支持 Ed25519、X25519、HKDF-SHA256 和 XChaCha20-Poly1305 的语言,其中包括适用于大多数语言的 libsodium 绑定。本页包含完整的 Python 参考实现和可运行的快速开始。

AeroNyx 聊天 API 是实时的吗?

发送是实时的:接收方的 App 在线时会立刻收到。通过 HTTPS 接收是轮询,要等你下次调用 GET /inbox/ 才会看到新消息。如需实时接收,请让同一个身份连接到聊天中继 WebSocket(wss://api.aeronyx.network/ws/relay/),它读取的是同一个队列。

我发送的消息会让 AeroNyx 用户收到通知吗?

目前不会。通过此 API 发送的消息不会触发推送通知,所以 App 已关闭的用户要在下次打开 App 时才会看到;App 在线的用户会立刻看到。

<!-- faq:end -->