AeroNyx Chat API Integration Guide

12 min read

Send and receive end-to-end encrypted AeroNyx messages over HTTPS: request signing, a runnable quick start, the sealed envelope format, error codes and test vectors.

The AeroNyx Chat API lets a server, bot or application send and receive end-to-end encrypted one-to-one messages with AeroNyx users over ordinary HTTPS. It does not use the AeroNyx Privacy Network, VPN, onion routing or anonymous mailbox.

Sending is immediate: if the recipient's App is connected, your message reaches it at once. Receiving through this API works by polling: GET /inbox/ returns whatever is waiting and never holds the request open. To receive in real time, connect the same identity to the Chat Relay WebSocket (wss://api.aeronyx.network/ws/relay/). See Real-time delivery.

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

How it works

  1. Create an identity. An Ed25519 key pair is your chat identity. The 32-byte public key, written as 64 lowercase hexadecimal characters, is your chat address. No AeroNyx account is needed.
  2. Sign every request. Each call carries an Authorization header signed with your key (Authentication).
  3. Seal each message. Encrypt and sign the message on your side and send only the resulting envelope (Message envelope).
  4. Send it with POST /messages/.
  5. Receive with GET /inbox/, decrypt and store each message, then confirm it with POST /acks/.
EndpointAuthenticationPurpose
GET /capabilities/noneService limits and supported features
POST /messages/requiredSend a sealed message
GET /inbox/requiredFetch unacknowledged messages
POST /acks/requiredConfirm messages you have stored

Quick start

This script sends a message from one new identity to another, reads it back, decrypts it and acknowledges it. Save the reference implementation below as aeronyx_chat.py, install the dependencies with pip install pynacl requests, and run it.

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

Authentication

Every endpoint except GET /capabilities/ requires this header:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex is your 64-character chat address.
  • unix-seconds must be within 60 seconds of the server clock.
  • nonce-hex is 16 random bytes written as 32 lowercase hex characters. Each nonce can be used once.
  • signature-hex is an Ed25519 signature, written as 128 hex characters, over this 32-byte digest:
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 is the full request path as sent, including the /api/relay/developer/v1 prefix and any query string. For example:

RequestPATH_WITH_QUERY to sign
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 are the bytes of the request body exactly as sent. JSON whitespace and key order therefore matter: serialize once, sign those bytes, and send those same bytes. A request without a body signs the SHA-256 of the empty string.

Authentication test vector

Use this to check your signing code before you call the API. The key is for testing only; never use it for real messages.

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 signatures are deterministic, so the same inputs must give exactly this signature_hex. If your body_sha256_hex differs, your body bytes differ.

Troubleshooting 401

The API answers every authentication failure with the same 401, so check these in order:

  1. The clock: unix-seconds is within 60 seconds of real time.
  2. The nonce is new for every request, 16 bytes, written as 32 lowercase hex characters.
  3. The signed path includes /api/relay/developer/v1 and the query string.
  4. The signed method is uppercase and matches the request.
  5. The signed body bytes are the bytes you send. Do not re-serialize the JSON after signing.
  6. Your digest and signature match the test vector above.

Endpoints

Service information

GET /capabilities/ needs no authentication and returns the current limits and supported features.

Send a message

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

All five fields are required and no others are allowed.

  • receiver_pubkey is the recipient's chat address. It must differ from your own.
  • message_id is a UUID. Use a new random UUID for each message and keep the exact body for retries. Sending the same message_id with different fields returns 409 idempotency_conflict.
  • discriminant is 11 for a one-to-one message that an AeroNyx App user should see.
  • sent_at is Unix seconds and must be within 300 seconds of server time.
  • payload_b64 is the sealed envelope described in Message envelope.

The relay checks only that payload_b64 is valid Base64 of 1 byte to 191 KiB. It cannot tell an envelope from other data, so a payload that is not a valid envelope is accepted here and silently dropped by the recipient's App. Test your envelope against the golden vector.

A successful request returns HTTP 202:

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

accepted means the relay stored the ciphertext durably. delivered_live is true when the relay handed the message to live delivery without an error. It is true even when the recipient has no connection open, so it does not tell you whether anyone is online. Neither field means the recipient has decrypted or displayed the message; a delivery receipt in your inbox does (Events in the inbox).

Fetch the inbox

GET /inbox/?limit=50

Returns up to 100 unacknowledged events. The same event can appear again after a timeout or a restart.

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

If has_more is true, acknowledge what you have stored and fetch again.

Events in the inbox

The inbox holds everything the relay has queued for your identity, not only new messages. An item without control_type is a message: open payload_b64 as described in Message envelope. An item with control_type is an event about a message. Check control_type before decrypting anything: receipts have no payload_b64.

control_typeMeaningmsg_id
message_receiptThe recipient's App decrypted and stored a message you sent.The message_id of your message
message_readThe recipient read your message. Sent only when their read receipts are on.The message_id of your message
message_editThe sender edited a message (target_msg_id, the same ID).The edited message's ID
message_revokeThe sender withdrew a message.The withdrawn message's ID
message_reactionA reaction to target_msg_id was added or removed.The reaction's ID

Edits, withdrawals and reactions are signed (payload_sig); their formats are described in AeroNyx Chat Relay Client Integration. Acknowledging an ID removes every queued item that carries it, so process a whole page before you acknowledge. Acknowledge event types you do not handle as well; otherwise they are returned again.

Acknowledge messages

POST /acks/

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

Acknowledge a message only after you have validated it, decrypted it and written it to durable storage. Unknown IDs are left out of the acknowledged list in the response.

Delivery model

  • Delivery is at least once. Deduplicate by control_type together with msg_id: a receipt carries the ID of the message it confirms.
  • Undelivered ciphertext is kept for 72 hours. Do not treat the service as permanent history.
  • Retry a failed send with the exact same body and a fresh authentication nonce.

Real-time delivery

Sending

POST /messages/ hands your message to every connection the recipient has open and, at the same time, keeps it queued until the recipient's App acknowledges it. A recipient whose App is connected receives it at once; one who is offline receives it the next time the App connects, within 72 hours.

Messages sent through this API do not trigger a push notification. A recipient whose App is closed is not alerted and sees the message the next time they open the App.

Receiving by polling

GET /inbox/ returns at once, whether or not anything is waiting. A message reaches you only when you next call it, so the delay you see is your polling interval. For conversations, receive over the WebSocket instead.

Receiving in real time

Connect the same identity to the Chat Relay WebSocket. No account or registration is needed.

  1. Open wss://api.aeronyx.network/ws/relay/ and log in with the same Ed25519 key, as described under Login in AeroNyx Chat Relay Client Integration.
  2. After login the relay first replays everything still queued for you (from_offline is true), then sends each new item as soon as it arrives. Messages arrive as relay_envelope frames with the same fields as an inbox message; receipts, edits, withdrawals and reactions arrive as frames whose type is the control_type shown above.
  3. Once you have stored an item, acknowledge it:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

The WebSocket and GET /inbox/ read the same queue: an item acknowledged on one is gone from the other, and POST /acks/ works for both. You can keep sending over HTTPS while you receive over the WebSocket.

Limits and errors

LimitValue
Sealed payload191 KiB (the Base64 JSON request stays below the request limit)
Request body256 KiB
Inbox page and acknowledgement batch100 items
Offline retention72 hours
StatusMeaning
400 invalid_jsonThe body is not a JSON object.
400 invalid_requestThe body does not contain exactly the five fields, receiver_pubkey is not 64 lowercase hex characters or equals your own address, discriminant is outside 1-255, message_id is not a UUID, or payload_b64 is not valid Base64.
400 invalid_payloadThe decoded payload is empty or larger than 191 KiB.
400 message_expiredsent_at differs from server time by more than 300 seconds.
401Authentication is missing, expired, invalid or replayed. See Troubleshooting 401.
403The recipient's policy rejected this sender.
409Idempotency conflict, or an identical request is still in progress.
429Sender rate limit reached. Wait for the time in the Retry-After header.
503Durable delivery or authentication is temporarily unavailable. Retry the exact body with a fresh nonce.

Privacy and visibility

Message content is end-to-end encrypted: it is sealed on your side with a key derived from the sender's and recipient's identities, which AeroNyx never holds, and the API stores and forwards only the ciphertext. To deliver messages the service can see the sender and receiver public keys, message time, size, delivery state and the IP address of each request. HTTPS does not hide this metadata from the service.

Message envelope

Messages that an AeroNyx App user should see use discriminant: 11 and an end-to-end sealed envelope in payload_b64. The relay never sees the key or the plaintext. All integers are little-endian.

1. Shared key

Both sides derive the same 32-byte key from their Ed25519 chat identities:

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)

With libsodium this is crypto_sign_ed25519_sk_to_curve25519, crypto_sign_ed25519_pk_to_curve25519 and crypto_scalarmult, followed by HKDF. Senders always use the HKDF key. Very old App versions sent with raw directly, so a receiver may try raw if the HKDF key fails to decrypt.

2. Plaintext

A text message is its UTF-8 text. Messages from the App that carry attachments, replies or forwards are a JSON object with "type": "aeronyx_message" and a text field; render text if you do not support the rest.

3. Encryption

XChaCha20-Poly1305 with a fresh random 24-byte nonce and no associated data. ciphertext below is the cipher output including its 16-byte tag.

4. Signature

The sender signs these 121 bytes with 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. Envelope

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 is the standard Base64 of these bytes. Note that content_type is 1 byte inside the signed data but 4 bytes in the envelope.

Receiving

Parse the envelope, check that receiver_pubkey is your identity, verify the signature against the envelope's sender_pubkey, then decrypt with the key derived from that sender. Drop the message if any step fails. The App drops live messages whose timestamp is more than 300 seconds from its clock.

Reference implementation (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"),
    }

Golden vector

Fixed keys, nonce, ID and time, for testing only. Never use these keys.

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))) must produce exactly this payload_b64, and open_(bob, payload_b64) must return the plaintext. This vector is verified against the AeroNyx App's own key agreement, signer and cipher, and against the node's strict envelope decoder.

<!-- faq:start -->

Frequently asked questions

What is the AeroNyx Chat API?

It is a versioned HTTPS API for exchanging end-to-end encrypted one-to-one messages with AeroNyx users from a server, bot or application. It has four endpoints: capabilities, send, inbox and acknowledge. It carries only sealed envelopes; the content is encrypted on your side.

Do I need an AeroNyx account to use the API?

No. An Ed25519 key pair is the identity. Generate it locally and sign each request with it; the public key is your chat address.

Do I sign /messages/ or the full path?

The full path as sent, starting with /api/relay/developer/v1, plus the query string if there is one. Signing only /messages/ produces a 401. The authentication test vector on this page shows the exact inputs.

Why do I get 401?

Every authentication failure returns the same 401. The usual causes are a clock more than 60 seconds off, a reused nonce, a signed path or body that differs from what was sent, or a lowercase HTTP method in the signed data. Work through the checklist under Troubleshooting 401 and compare your digest with the test vector.

Why does the API return 202 but the message never appears in the AeroNyx App?

The relay accepts any valid Base64 payload, but the App only shows a sealed envelope that it can verify and decrypt. Check your payload_b64 against the golden vector on this page, make sure the envelope's receiver_pubkey is the recipient, and keep the envelope timestamp within 300 seconds of the current time. Sending plain text encoded as Base64 is accepted but never displayed.

Why does the API return invalid_request?

The most common causes are sending to your own public key, a body that does not contain exactly the five documented fields, a message_id that is not a UUID, or a discriminant outside 1-255. See the error table under Limits and errors.

Can AeroNyx read messages sent through this API?

No. Envelopes are encrypted with a key derived from the sender's and recipient's identities, which AeroNyx never holds. The service can see delivery metadata such as public keys, times, sizes and IP addresses.

How long are undelivered messages kept?

Up to 72 hours. Acknowledge each message only after you have decrypted and stored it; unacknowledged messages can be delivered again.

Which programming languages can I use?

Any language with Ed25519, X25519, HKDF-SHA256 and XChaCha20-Poly1305, which includes libsodium bindings for most languages. This page includes a complete Python reference implementation and a runnable quick start.

Is the AeroNyx Chat API real-time?

Sending is: a recipient whose App is connected gets your message at once. Receiving over HTTPS is polling, so you see new messages when you next call GET /inbox/. For real-time receiving, connect the same identity to the Chat Relay WebSocket at wss://api.aeronyx.network/ws/relay/; it reads the same queue.

Does an AeroNyx user get a notification for a message I send?

Not at present. Messages sent through this API do not trigger a push notification, so a user whose App is closed sees your message the next time they open it. A user whose App is connected sees it at once.

<!-- faq:end -->