POST /api/embed/submissions
Finishes a wallet submission. The server verifies the SIWS signature, runs policy checks (CA gate, cooldown, trust ladder), and either queues the submission for owner review or auto-approves it.
Used after POST /siws/nonce.
Request
POST /api/embed/submissions HTTP/1.1
Host: walletsleuth.app
Authorization: Bearer ws_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Origin: https://example.com
Content-Type: application/json
Cookie: embed_session=<session-cookie>
{
"trackingPageId": "abc123",
"walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"nickname": "Early holder",
"nonceId": "6f7a1e4a-5b3f-4b3e-8a2c-e2d8c1b4f7a1",
"signature": "PvV3kK8Q…base64…YAzQ=="
}
Headers
| Header | Required | Notes |
|---|---|---|
Authorization | yes | Bearer <api_key>. Same key that minted the nonce. |
Origin | yes for public keys | Must be on the key's allowlist. |
Content-Type | yes | application/json. |
Cookie: embed_session=... | optional but recommended | Auto-set by the iframe. Enables per-session rate limiting and lets the visitor see their submissions in GET /session/submissions. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
trackingPageId | string | yes | Must match the nonce's binding. |
walletAddress | string | yes | Base58 address. Must match the nonce's binding. |
nickname | string | yes | Public display name for the wallet. See validation rules below. |
nonceId | string | yes | Value returned by POST /siws/nonce. |
signature | string | yes | Base64 Ed25519 signature of the message returned with the nonce. Exactly 64 raw bytes. |
Nickname validation
Nicknames must be 1–32 characters and may contain letters, numbers, spaces, and . _ -. The server trims and Unicode-normalizes the value before checking.
Violations return:
400 invalid_nickname_length— empty or over 32 chars.400 invalid_nickname_chars— any disallowed character.
Success response
201 Created:
{
"submissionId": "WvE7aB9c2D4f…",
"status": "pending"
}
status is one of:
| Value | Meaning |
|---|---|
pending | Submission is queued for owner review. |
auto_approved | Submission was approved by policy (trust-ladder promotion or auto approval mode). |
The owner's dashboard surfaces pending submissions. Auto-approved submissions are live immediately.
Error responses
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_json | Body wasn't valid JSON. |
| 400 | missing_fields | One of the required fields is missing or wrong type. |
| 400 | invalid_wallet_address | walletAddress isn't a valid base58 address. |
| 400 | invalid_nickname_length | Empty after trimming, or > 32 chars. |
| 400 | invalid_nickname_chars | Nickname contains disallowed characters. |
| 400 | invalid_nonce | Nonce is unknown, already consumed, or expired. |
| 400 | nonce_binding_mismatch | Nonce was issued for a different page, wallet, or API key. |
| 401 | invalid_signature | Signature didn't verify against the rebuilt SIWS message. |
| 401 | missing_key / invalid_key / revoked_key | See Authentication. |
| 401 | missing_origin | Public key with no Origin/Referer. |
| 403 | origin_not_allowed | Origin not on the key's allowlist. |
| 403 | embed_disabled | Tracking page is private or has embedding disabled. |
| 404 | page_not_found | Tracking page doesn't exist. |
| 409 | duplicate | This wallet already has a pending, approved, or auto_approved submission on this page. |
| 422 | ca_gate_failed | CA holding gate failed. Response body includes balance and minBalance. |
| 429 | cooldown_active | Wallet is on cooldown after a prior rejection. Retry-After included. |
| 429 | queue_full | Page's pending queue is at capacity. Retry-After included. |
| 429 | rate_limit_key / rate_limit_ip | Global rate limits. Retry-After included. |
| 429 | rate_limit_session | Session submit limit exceeded. Retry-After included. |
CA gate response detail
422 ca_gate_failed bodies include the observed balance:
{
"error": "ca_gate_failed",
"balance": 0,
"minBalance": 1000
}
Use this to give the visitor a clear "you need at least N tokens to submit" message.
Session cookie behavior
If the request arrives with a valid embed_session cookie (set automatically by the iframe route), the submission is tagged with its session ID. This is what powers GET /session/submissions: the visitor sees "Your submissions" with live status updates.
Custom UIs that don't load the iframe and therefore don't have the cookie will still submit successfully — session-scoped rate limits just don't apply.
Example (fetch, end-to-end)
// 1. Nonce
const nonceRes = await fetch(
'https://walletsleuth.app/api/embed/siws/nonce',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${publicKey}`,
},
credentials: 'include',
body: JSON.stringify({ trackingPageId, walletAddress }),
},
);
const { nonceId, message } = await nonceRes.json();
// 2. Sign
const sigBytes = await wallet.signMessage(new TextEncoder().encode(message));
const signature = Buffer.from(sigBytes).toString('base64');
// 3. Submit
const submitRes = await fetch(
'https://walletsleuth.app/api/embed/submissions',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${publicKey}`,
},
credentials: 'include',
body: JSON.stringify({
trackingPageId,
walletAddress,
nickname: 'Early holder',
nonceId,
signature,
}),
},
);
if (submitRes.status === 201) {
const { submissionId, status } = await submitRes.json();
// status: 'pending' | 'auto_approved'
}
Related
- POST /siws/nonce — the preceding call.
- GET /session/submissions — read back what the visitor submitted.
- Concepts → Verification policy — what governs the
statusfield. - Concepts → Trust ladder — when submissions auto-approve.
- Concepts → Cooldowns and rate limits.