Skip to main content
Bulk Send enables one-time batch crypto transfers to multiple recipient addresses. It is suitable for payroll payouts, rebate/reward distributions, partner settlements, and revenue sharing for e-commerce/advertising/content platforms. You only need to submit a request containing multiple bulk send items. Cobo will validate each item (address format checks and compliance verification such as KYA) and then decide whether to proceed with on-chain transfers based on the execution mode.

Prerequisites

Before you start, make sure you have completed the following:
  • Ensure the source_account has sufficient balance.
  • (Recommended) Configure a Webhook Endpoint to receive Bulk Send status updates and drive an automated retry workflow.

1. How to send in batch

1.1 Create a Bulk Send

To create a Bulk Send, provide:
  • source_account: Source account ID (e.g., M1001)
  • execution_mode: Execution mode (Strict / Partial)
  • payout_params: Array of bulk send items. Each item includes:
    • token_id
    • receiving_address
    • amount
    • description (recommended: include business-traceable info such as payroll slip No./user ID/batch ID)
curl -X POST "https://api.example.com/v1/payments/bulk_sends" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <YOUR_ACCESS_TOKEN>" \
  -d '{
    "source_account": "M1001",
    "execution_mode": "Partial",
    "description": "january salary",
    "payout_params": [
      {
        "token_id": "ETH_USDT",
        "receiving_address": "0xabc123456789def0000000000000000000000000",
        "amount": "500.00",
        "description": "Salary 2026-01 | eid=10001"
      },
      {
        "token_id": "TRX_USDT",
        "receiving_address": "TQx8yq9hFJt2cNxxxxxxxxyyyyyyyyyy",
        "amount": "320.50",
        "description": "Salary 2026-01 | eid=10002"
      }
    ]
  }'

1.2 How to choose execution_mode

After receiving a Bulk Send request, Cobo first validates all addresses in the request (including format validation and compliance checks such as KYA). execution_mode determines whether Cobo continues with on-chain transfers for valid items when any item fails validation.

Behavior: Strict vs Partial

execution_modeIf any address fails validation (e.g., KYA rejected)On-chain transfer resultWhat you need to do
StrictIf any bulk send item has an invalid addressNo on-chain transfers will be initiated (the entire batch is not executed)Replace the problematic address(es) and resubmit the entire Bulk Send (must include all items
PartialAllows some invalid addressesTransfers will be executed for all validated items; invalid items will be skippedQuery failed items, replace addresses, and resend only the failed items
  • Use Strict (strong consistency: all-or-nothing)
    • Partner settlements (must take effect consistently within the same batch)
    • Vendor payments (finance requires batch-level consistency)
    • Any scenario where partial success is unacceptable
  • Use Partial (high throughput: partial failures allowed, retry later)
    • Global payroll payouts (large volume; a few bad addresses should not block the whole batch)
    • Rebates/rewards airdrops (mixed address quality)
    • Tips/revenue distribution for content platforms; merchant payout distribution for e-commerce/ads platforms (retry is acceptable)
Impact summary:
  • Strict improves financial consistency, but increases operational overhead due to “one bad address forces a full resubmission”.
  • Partial scales better and is more automation-friendly, but requires a retry workflow for failed items.

1.3 Smart contract mechanism for Bulk Send

When executing a Bulk Send, Cobo uses smart contracts to perform batch transfers. Understanding this mechanism helps you correctly verify contract call information in your callback handler.

Approve operation (triggered when allowance is insufficient)

When the smart contract’s allowance for a token is insufficient, an approve call is first triggered on the token contract:
  • Function called: approve
  • calldata parameters:
    • _spender: The batch transfer contract address (see table below)
    • _value: The transfer amount

Transfer operation

After the approve completes, the actual transfer calls the sendToken function on the batch transfer contract:
  • Function called: sendToken
  • calldata parameters:
    • token: The token contract address (see table below)
    • recipients: List of recipient addresses
    • values: Corresponding list of transfer amounts

Contract address reference

Since the contract addresses are fixed for each token, it is recommended to verify the contract addresses and function call information in your callback handler to ensure the callback completes correctly.
ChainToken IDToken contract addressBatch transfer contract address
EthereumETH_USDT0xdac17f958d2ee523a2206206994597c13d831ec70x3d963e23a9229d2acd25e9ffc358be1a35460ecc
EthereumETH_USDC0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb480x3d963e23a9229d2acd25e9ffc358be1a35460ecc
ArbitrumARBITRUM_USDT0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb90x3d963e23a9229d2acd25e9ffc358be1a35460ecc
ArbitrumARBITRUM_USDCOIN0xaf88d065e77c8cc2239327c5edb3a432268e58310x3d963e23a9229d2acd25e9ffc358be1a35460ecc
BaseBASE_USDT0xfde4c96c8593536e31f229ea8f37b2ada2699bb20x3d963e23a9229d2acd25e9ffc358be1a35460ecc
BaseBASE_USDC0x833589fcd6edb6e08f4c7c32d4f71b54bda029130x3d963e23a9229d2acd25e9ffc358be1a35460ecc
PolygonMATIC_USDT0xc2132d05d31c914a87c6611c10748aeb04b58e8f0x3d963e23a9229d2acd25e9ffc358be1a35460ecc
PolygonMATIC_USDC20x3c499c542cef5e3811e1192ce70d8cc03d5c33590x3d963e23a9229d2acd25e9ffc358be1a35460ecc
BNB Smart ChainBSC_USDT0x55d398326f99059ff775485246999027b31979550x3d963e23a9229d2acd25e9ffc358be1a35460ecc
BNB Smart ChainBSC_USDC0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d0x3d963e23a9229d2acd25e9ffc358be1a35460ecc
TRONTRON_USDTTR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6tTEmDa9FY9YBCg1HPL41RUnJorXRRV3rX7A

2. Webhook-driven automation workflow (failure notification → query items → batch resend)

2.1 Subscribe to Bulk Send status update events

Cobo triggers a webhook event when the Bulk Send status transitions to any of the following:
  • Completed
  • PartiallyCompleted
  • Failed
Webhook payload example (Bulk Send Notification):
{
    "data_type": "PaymentBulkSend",
    "bulk_send_id": "8ecf2933-5ef2-4f1d-9b59-3f4ff017d16a",
    "request_id": "SendBatch-001",
    "source_account": "M1001",
    "execution_mode": "Partial",
    "status": "Completed",
    "created_timestamp": 1768875988,
    "updated_timestamp": 1768878524
}
Status can be one of:
  • Pending
  • Validating
  • Transferring
  • Completed
  • PartiallyCompleted
  • Failed
Recommendations
  • Trigger the resend workflow only when the status enters PartiallyCompleted or Failed. During Validating / Transfering, you may only display or monitor progress.
  • Implement webhook signature verification and idempotency handling (the same event may be replayed).

2.2 After a failure notification: query Bulk Send item details

After receiving a PartiallyCompleted or Failed webhook, call List bulk send items to query the status of each bulk send item:
  • Path parameter: bulk_send_id
  • Operation: list_bulk_send_items
  • Summary: List bulk send items
(Example pseudo endpoint)
curl -X GET "https://api.example.com/v1/payments/bulk_sends/{bulk_send_id}/items" \
  -H "Authorization: Bearer <YOUR_ACCESS_TOKEN>"
Each item includes two key status fields:
  • status (execution status of the item)
  • validation_status (address/compliance validation status)
Rules to focus on:
  • If status = Failed or NotExecuted → the item failed
  • If validation_status = ValidationFailed → the address did not pass Cobo KYA validation
For both cases, replacing the address is recommended.

2.3 Automated retry: replace addresses and resend in batch

Resend strategy depends on execution_mode:

A) Previous batch used Strict

  • Under Strict, if any address has an issue, no on-chain transfer is executed for the entire batch.
  • Therefore, on retry (resubmission), you must include all items (not only the failed ones).
Steps:
  1. Query items and identify those with ValidationFailed or Failed/NotExecuted
  2. Replace the addresses for those items (or fix other parameters causing failures)
  3. Resubmit a new Bulk Send containing all items

B) Previous batch used Partial

  • Under Partial, successful items have already been transferred.
  • Therefore, on retry, submit only failed items (reduces the risk of duplicate transfers).
Steps:
  1. Query items and filter failed items (status=Failed/NotExecuted or validation_status=ValidationFailed)
  2. Replace the addresses for failed items
  3. Create a new Bulk Send containing only these failed items

3. Reference implementation: webhook handling + auto resend (example)

This example demonstrates a typical workflow:
  1. Receive webhook
  2. If status is PartiallyCompleted / Failed, query items
  3. Filter failed items and replace addresses (address replacement is handled by your business system)
  4. Create a new Bulk Send for resend
Note: This is illustrative pseudo-code (Node.js style). Adjust based on your actual SDK and signature verification mechanism.
import express from "express";
import fetch from "node-fetch";

const app = express();
app.use(express.json());

function isTerminalFailedStatus(bulkStatus) {
    return bulkStatus === "Failed" || bulkStatus === "PartiallyCompleted";
}

function isItemFailed(item) {
    return item.status === "Failed" || item.status === "NotExecuted" || item.validation_status === "ValidationFailed";
}

async function replaceAddressForFailedItem(item) {
    const newAddress = await lookupNewAddressFromYourSystem(item);
    return { ...item, receiving_address: newAddress };
}

app.post("/webhooks/cobo", async (req, res) => {

    const event = req.body;
    const { bulk_send_id, status, source_account, execution_mode } = event;

    if (!isTerminalFailedStatus(status)) {
        return res.status(200).send("ok");
    }

    const itemsResp = await fetch(`https://api.example.com/v1/payments/bulk_sends/${bulk_send_id}/items`, {
        method: "GET",
        headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN}` }
    });
    const items = await itemsResp.json(); 

    const failedItems = items.filter(isItemFailed);

    const itemsToResend = [];
    if (isTerminalFailedStatus(status)) {
        for (const item of items) {
            if (item.status === 'Failed') {
                itemsToResend.push(item);  
            } else if (item.status === 'NotExecuted' && item.validation_status === 'ValidationFailed') {
                itemsToResend.push(await replaceAddressForFailedItem(item));  
            }
        }
    }

    const createResp = await fetch(`https://api.example.com/v1/payments/bulk_sends`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${process.env.ACCESS_TOKEN}`
        },
        body: JSON.stringify({
            source_account,
            execution_mode: "Partial", 
            payout_params: itemsToResend.map(i => ({
                token_id: i.token_id,
                receiving_address: i.receiving_address,
                amount: i.amount,
                description: i.description
            }))
        })
    });

    const created = await createResp.json();
    console.log("Resend bulk send created:", created);

    return res.status(200).send("ok");
});

app.listen(3000, () => console.log("Webhook listener on :3000"));

4. Best practices

  • Always include business-traceable identifiers in description (e.g., batch_id + user_id/employee_id + period) for auditing, reconciliation, diagnosing failures, and address replacement.
  • Use different resend strategies for Strict vs Partial:
    • Strict:retry must include all items
    • Partial:retry only failed items (avoid duplicate payouts)
  • Webhook idempotency: use bulk_send_id + status + updated_timestamp as an idempotency key to avoid duplicated resend triggers.
  • Failure classification:
    • validation_status=ValidationFailed:prioritize replacing the address
    • status=Failed/NotExecuted: also recommended to replace the address (per your rule), and retain the failure reason for risk control and user messaging