AeroNyx 聊天 API 集成指南
通过 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
工作原理
- 创建身份。 一对 Ed25519 密钥就是你的聊天身份。32 字节的公钥以 64 个小写十六进制字符表示,即为你的聊天地址。无需 AeroNyx 账户。
- 为每个请求签名。 每次调用都携带用你的密钥签名的
Authorization请求头(认证)。 - 封装每条消息。 在你这一侧加密并签名消息,只发送生成的信封(消息信封)。
- 发送: 使用
POST /messages/发送。 - 接收: 使用
GET /inbox/获取,解密并保存每条消息,然后使用POST /acks/确认。
| 端点 | 认证 | 用途 |
|---|---|---|
GET /capabilities/ | 无 | 服务限制和支持的功能 |
POST /messages/ | 必需 | 发送已封装的消息 |
GET /inbox/ | 必需 | 获取未确认的消息 |
POST /acks/ | 必需 | 确认你已保存的消息 |
快速开始
此脚本会从一个新身份向另一个新身份发送消息,再将其读回、解密并确认。请将下方的参考实现保存为 aeronyx_chat.py,使用 pip install pynacl requests 安装依赖,然后运行。
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/ 外,每个端点都需要此请求头:
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 个十六进制字符表示:
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 之前,用它来检查你的签名代码。该密钥仅用于测试,切勿用于真实消息。
{
"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,因此请按顺序检查:
- 时钟:
unix-seconds与真实时间相差不超过 60 秒。 - 每个请求的 nonce 都是新的,长 16 字节,以 32 个小写十六进制字符表示。
- 签名的路径包含
/api/relay/developer/v1和查询字符串。 - 签名的方法为大写,并与请求一致。
- 签名的请求体字节就是你发送的字节。签名之后不要重新序列化 JSON。
- 你的摘要和签名与上面的测试向量一致。
端点
服务信息
GET /capabilities/ 无需认证,返回当前的限制和支持的功能。
发送消息
POST /messages/
{
"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:
{
"message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
"accepted": true,
"delivered_live": false
}
accepted 表示中继已将密文持久化存储。delivered_live 为 true 只表示中继已把消息交给实时投递且没有出错;即使接收方没有任何连接,它也是 true,所以不能用来判断对方是否在线。两者都不代表接收方已经解密或显示了该消息;收件箱里的投递回执才代表这件事(见“收件箱中的事件”)。
获取收件箱
GET /inbox/?limit=50
最多返回 100 个未确认的事件。同一个事件在超时或重启之后可能再次出现。
{
"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/
{
"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,无需账号或注册。
- 打开
wss://api.aeronyx.network/ws/relay/,并用同一把 Ed25519 密钥登录,方法见 AeroNyx 聊天中继客户端集成 的“登录”一节。 - 登录后,中继会先重发所有仍在队列中的条目(
from_offline为true),之后每个新条目一到就立刻发送。消息以relay_envelope帧送达,字段与收件箱中的消息相同;回执、编辑、撤回和表情回应则以type等于上表control_type的帧送达。 - 保存条目后,确认它:
{"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_expired | sent_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 字节密钥:
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 字节签名:
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. 信封
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)
"""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 和时间,仅用于测试。切勿使用这些密钥。
{
"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 自身的密钥协商、签名器和密码实现,以及节点的严格信封解码器进行验证。
常见问题
什么是 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 -->