Andamio Logo
Developer Guides

Error Handling

Recovery patterns for failed and expired Andamio transactions

Error Handling

How to handle errors and recover from failed transactions.

State Machine States

Transactions can reach three terminal states:

StateMeaningRecovery
updatedSuccess — DB syncedNone needed
failedDB sync failed after retriesUsually auto-heals; see below
expiredNot confirmed on-chain in timeRebuild and resubmit

Common API Errors

CodeMeaningResolution
400Bad request / validation errorCheck request body against API Reference
401Invalid or expired JWTRe-authenticate user via wallet signature
403Insufficient permissionsUser lacks required role (e.g., not a teacher)
409Duplicate registrationTransaction already registered — safe to ignore
422Unprocessable entitySemantic error (e.g., module doesn't exist)
500Internal server errorRetry with exponential backoff
502Upstream service errorCardano node issue — retry after delay

Five Failure Modes

Every transaction failure falls into one of five categories, each with a distinct recovery action:

#FailureHow to DetectRecovery
1Build errorHTTP 4xx from tx buildFix request body, retry build
2Sign errorLocal error from tx signFix key path or permissions, retry sign
3Submit rejectionCardano rejects the transactionFix the condition that caused rejection, rebuild fresh
4Chain expiryState = expired, confirmed_at is nullCheck Andamioscan first, then rebuild if truly not on-chain
5Off-chain sync failureState = failed, confirmed_at is setDo NOT retry. The chain TX succeeded. Off-chain sync will auto-retry.

Never rebuild a transaction in failed state. If confirmed_at has a value, the transaction is on-chain. Rebuilding would create a duplicate action. The gateway's self-healing reconciler retries the off-chain sync automatically.

Build and Sign Errors (1-2)

These are pre-submission errors — no on-chain state was created. Fix the input and retry safely.

Submit Rejection (3)

Cardano rejected the transaction (e.g., insufficient funds, invalid inputs, script failure). The old unsigned TX is now stale — you must rebuild from scratch because UTxO inputs may have changed.

Commitment State Expiry

When a transaction expires, most commitment states revert to their previous value:

State Before TXReverts ToNotes
SUBMITPre-submission stateSafe revert
ASSESSSUBMITTEDSafe revert
CLAIMACCEPTEDSafe revert
PENDING_TX_COMMITNo revertNo earlier state exists — this was the first enrollment
PENDING_TX_LEAVENo revertReverting would undo an intentional user action

The two non-reverting states require special handling:

  • PENDING_TX_COMMIT: The commitment record stays in pending state. Your app must let the user retry the transaction or delete the commitment.
  • PENDING_TX_LEAVE: The commitment record stays in pending state. The user intended to leave — don't automatically re-enroll them.

Recovery Patterns

Expired Transaction

A transaction expires when it's not confirmed on-chain within the timeout window (typically 2 hours).

Before rebuilding, check if it actually confirmed:

async function handleExpired(txHash: string) {
  // Check Andamioscan to see if TX actually confirmed
  const scanRes = await fetch(
    `https://preprod.andamioscan.io/api/tx/${txHash}`
  );

  if (scanRes.ok) {
    // TX is on-chain! State machine missed it.
    // The self-healing reconciler will fix this on next read.
    console.log("TX confirmed but not detected — will auto-heal");
    return { action: "wait", reason: "auto-heal" };
  }

  // TX truly didn't confirm — safe to rebuild
  return { action: "rebuild", reason: "not-on-chain" };
}

Why this matters: If you rebuild without checking, you might submit a duplicate action (e.g., double-minting a credential).

Failed DB Sync

A failed state means the transaction confirmed on-chain but the database update failed after multiple retries.

Usually no action needed:

The Gateway has a self-healing reconciler that detects and repairs inconsistencies when related data is accessed. Your next API call that reads this data will trigger the repair.

If time-sensitive:

// Force a read to trigger reconciliation
await fetch(`${API}/course/${courseId}/commitments`, { headers });
// The reconciler will notice the on-chain state and sync it

Network Errors

For transient network issues, implement retry with exponential backoff:

async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const res = await fetch(url, options);
      if (res.ok || res.status < 500) return res;
    } catch (e) {
      if (i === maxRetries - 1) throw e;
    }

    // Exponential backoff: 1s, 2s, 4s
    await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
  }
}

Wallet Errors

Common wallet-related issues:

ErrorCauseResolution
User rejectedUser cancelled signingPrompt to try again
Insufficient fundsNot enough ADAShow required amount
TX too largeToo many inputs/outputsContact support
Collateral not setdApp collateral not configuredPrompt user to set collateral in wallet
try {
  const signedTx = await wallet.signTx(unsignedTx);
} catch (e) {
  if (e.message?.includes("user declined")) {
    // User cancelled — don't retry automatically
    return { error: "cancelled", retry: false };
  }
  if (e.message?.includes("insufficient")) {
    const required = parseRequiredAda(e.message);
    return { error: "insufficient_funds", required };
  }
  throw e; // Unknown error
}

Idempotency

Andamio's state machine is designed for safe retries:

  • Registration is idempotent: Registering the same tx_hash twice returns 409 Conflict but doesn't create duplicate records
  • DB syncs are idempotent: If a sync is interrupted and retried, it produces the same result
  • Draft upserts: Creating a commitment draft that already exists updates it rather than duplicating

This means if you're unsure whether an operation completed, it's safe to retry.

Debugging

Check Transaction Status

curl -H "X-API-Key: $API_KEY" \
  https://preprod.api.andamio.io/api/v2/tx/status/$TX_HASH

View Pending Transactions

curl -H "X-API-Key: $API_KEY" \
     -H "Authorization: Bearer $JWT" \
  https://preprod.api.andamio.io/api/v2/tx/pending

Check On-Chain State

For preprod:

curl https://preprod.andamioscan.io/api/tx/$TX_HASH

For mainnet:

curl https://andamioscan.io/api/tx/$TX_HASH

Best Practices

  1. Always check before rebuilding expired TXs — Avoid duplicate on-chain actions
  2. Implement SSE for real-time updates — Better UX than polling
  3. Store tx_hash immediately after submit — Enables recovery if registration fails
  4. Use draft-first flow — Provides instant feedback and safer recovery
  5. Log state transitions — Helps debug issues in production

Next Steps