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

HeaderRequiredNotes
AuthorizationyesBearer <api_key>. Same key that minted the nonce.
Originyes for public keysMust be on the key's allowlist.
Content-Typeyesapplication/json.
Cookie: embed_session=...optional but recommendedAuto-set by the iframe. Enables per-session rate limiting and lets the visitor see their submissions in GET /session/submissions.

Body

FieldTypeRequiredNotes
trackingPageIdstringyesMust match the nonce's binding.
walletAddressstringyesBase58 address. Must match the nonce's binding.
nicknamestringyesPublic display name for the wallet. See validation rules below.
nonceIdstringyesValue returned by POST /siws/nonce.
signaturestringyesBase64 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:

ValueMeaning
pendingSubmission is queued for owner review.
auto_approvedSubmission 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

StatuserrorMeaning
400invalid_jsonBody wasn't valid JSON.
400missing_fieldsOne of the required fields is missing or wrong type.
400invalid_wallet_addresswalletAddress isn't a valid base58 address.
400invalid_nickname_lengthEmpty after trimming, or > 32 chars.
400invalid_nickname_charsNickname contains disallowed characters.
400invalid_nonceNonce is unknown, already consumed, or expired.
400nonce_binding_mismatchNonce was issued for a different page, wallet, or API key.
401invalid_signatureSignature didn't verify against the rebuilt SIWS message.
401missing_key / invalid_key / revoked_keySee Authentication.
401missing_originPublic key with no Origin/Referer.
403origin_not_allowedOrigin not on the key's allowlist.
403embed_disabledTracking page is private or has embedding disabled.
404page_not_foundTracking page doesn't exist.
409duplicateThis wallet already has a pending, approved, or auto_approved submission on this page.
422ca_gate_failedCA holding gate failed. Response body includes balance and minBalance.
429cooldown_activeWallet is on cooldown after a prior rejection. Retry-After included.
429queue_fullPage's pending queue is at capacity. Retry-After included.
429rate_limit_key / rate_limit_ipGlobal rate limits. Retry-After included.
429rate_limit_sessionSession 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