Async Operations
All blockchain operations (deploy, mint, transfer, burn, etc.) are asynchronous. The API returns immediately while the operation is processed in the background. This design is necessary because blockchain transactions can take seconds to minutes to confirm.
Transaction Lifecycle
┌─────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ pending │ ──→ │ submitted │ ──→ │ confirmed │ or │ failed │
└─────────┘ └───────────┘ └───────────┘ └───────────┘
↓ ↓ ↓ ↓
Queued for Sent to RPC Included in Reverted or
processing (has tx_hash) a block timed out
| Status | Meaning | Has tx_hash? |
|---|---|---|
pending | Job queued, not yet submitted | No |
submitted | Sent to blockchain RPC | Yes |
confirmed | Included in a block and confirmed | Yes |
failed | Reverted or could not be submitted | Sometimes |
Token Deploys
Token deployment uses "deploying" instead of "pending" as the initial status to distinguish from regular transaction operations.
Request Flow
1. Client sends POST request
→ API validates input (returns 400 if invalid)
→ Saves transaction to PostgreSQL with status: "pending"
→ Enqueues job to BullMQ
→ Returns { id: "tx_abc123", status: "pending" }
2. tx-submit worker picks up job
→ Builds transaction (to, data, gas, nonce)
→ Signs with tenant's HD wallet
→ Submits to blockchain RPC
→ Updates status: "submitted", sets tx_hash
3. tx-confirm worker monitors
→ Polls for block inclusion
→ Updates status: "confirmed" (with block_number, gas_used)
→ Fires webhook event: "transaction.confirmed"
Tracking Transaction Status
Option 1: Polling
const tx = await urblock.tokens.mint("tok_abc123", {
to: "0x1234...",
amount: "1000000000000000000",
idempotency_key: "mint-001",
});
// Poll until final state
let result = tx;
while (result.status === "pending" || result.status === "submitted") {
await new Promise(r => setTimeout(r, 3000)); // Wait 3 seconds
result = await urblock.transactions.get(tx.id);
}
if (result.status === "confirmed") {
console.log(`Confirmed in block ${result.block_number} — tx: ${result.tx_hash}`);
} else {
console.error(`Failed: ${result.error_message}`);
}
Option 2: Webhooks (Recommended)
// Register a webhook (one-time setup)
await urblock.webhooks.create({
url: "https://api.example.com/webhooks/urblock",
events: ["transaction.confirmed", "transaction.failed"],
});
// Your webhook handler receives events in real time
app.post("/webhooks/urblock", (req, res) => {
const event = req.body;
if (event.type === "transaction.confirmed") {
console.log(`TX ${event.data.id} confirmed — hash: ${event.data.tx_hash}`);
} else if (event.type === "transaction.failed") {
console.error(`TX ${event.data.id} failed — ${event.data.error_message}`);
}
res.status(200).send("ok");
});
Retry Behavior
Workers automatically retry failed jobs with exponential backoff:
| Operation | Max Retries | Base Delay | Max Delay |
|---|---|---|---|
| Transaction submit | 3 | 5s | 40s |
| Transaction confirm | 10 | 10s | ~2.8h |
| Contract deploy | 3 | 10s | 80s |
| Webhook delivery | 5 | 30s | ~32 min |
Timeout Expectations
| Network | Average Confirmation | Max Wait |
|---|---|---|
| Polygon | ~2-5 seconds | 30s |
| Ethereum | ~12-15 seconds | 5 min |
| Arbitrum | ~1-3 seconds | 30s |
| Base | ~2-4 seconds | 30s |
Best Practices
- Use webhooks for production — polling is fine for dev, but webhooks are more efficient
- Include idempotency keys — safe retries if your client times out before getting the response
- Don't assume immediate confirmation — always check final status
- Handle failures gracefully — reverted transactions are normal; implement retry logic
- Log transaction IDs — store
tx_idin your database to correlate with webhook events
Next Steps
- Transactions API — submit and track transactions
- Webhooks Overview — how webhook notifications work
- Idempotency — prevent duplicate operations
- Architecture — understand the full system design