> ## Documentation Index
> Fetch the complete documentation index at: https://cobo.com/payments/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk send

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)

```shell theme={null}
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 what happens in two situations: when **any** item fails validation, and — for `Strict` batches only — when the on-chain transfer step completes only partially (a partially completed sweep). `execution_mode` does not guarantee that every item succeeds once validation passes, and it does not provide transaction-level atomicity beyond these two situations. Always check each item's own `status` and `validation_status` instead of assuming a single outcome for the whole batch.

#### **Behavior: Strict vs Partial**

| execution\_mode | When a problem occurs                                       | Batch and item outcome                                                                                                                                                  | What you need to do                                                                                    |
| --------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `Strict`        | **Any** item fails validation (for example, KYA rejected)   | The batch status becomes `Failed` and **every** item is marked `NotExecuted`                                                                                            | Replace the problematic address(es) and resubmit the **entire** Bulk Send (must include **all** items) |
| `Strict`        | The on-chain transfer step (sweep) completes only partially | The batch status becomes `Failed`                                                                                                                                       | Inspect each item's `status` individually before deciding whether and how to resubmit                  |
| `Partial`       | One or more items fail validation                           | The validation failure is isolated: failed items are marked `NotExecuted` with `validation_status: ValidationFailed`; all validated items continue to on-chain transfer | Query failed items, replace addresses, and resend **only** the failed items                            |

<Note>
  `execution_mode` governs only the failure paths described above. It does not guarantee that a `Partial` batch's validated items will all succeed on-chain, or that a `Strict` batch is atomic beyond these paths. Always inspect each item's `status` and `validation_status` individually, and check the batch `status` (`PartiallyCompleted` or `Failed`) before deciding whether to resend.
</Note>

#### **Recommended scenarios**

* **`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)`** — recommended whenever a single item's validation failure must not block the rest of a valid batch:
  * 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 by failing the whole batch on any validation failure (marking every item `NotExecuted`) or a partially completed sweep (batch status `Failed`), but increases operational overhead: a validation failure forces a full resubmission, while a partially completed sweep requires you to inspect each item's `status` individually before deciding what to resubmit.
* `Partial` isolates validation failures to the affected items so the rest of the batch proceeds, which scales better and is more automation-friendly, but requires a retry workflow for the failed items.

### **1.3 Smart contract mechanism for Bulk Send**

For Bulk Send on Ethereum, Cobo uses smart contracts to perform batch transfers. For other chains, Cobo uses regular transactions instead. In both cases, Cobo sends a callback message to your registered callback endpoint for each operation so that you can inspect the details and approve or deny execution. The exact mechanism may also vary based on your organization's configuration — contact Cobo support for details. To learn how to set up and register a callback endpoint, see [Introduction to Callbacks](/payments/en/developer-tools/callback).

#### **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.

| Chain           | Token ID          | Token contract address                       | Batch transfer contract address              |
| --------------- | ----------------- | -------------------------------------------- | -------------------------------------------- |
| Ethereum        | ETH\_USDT         | `0xdac17f958d2ee523a2206206994597c13d831ec7` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Ethereum        | ETH\_USDC         | `0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Arbitrum        | ARBITRUM\_USDT    | `0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Arbitrum        | ARBITRUM\_USDCOIN | `0xaf88d065e77c8cc2239327c5edb3a432268e5831` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Base            | BASE\_USDT        | `0xfde4c96c8593536e31f229ea8f37b2ada2699bb2` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Base            | BASE\_USDC        | `0x833589fcd6edb6e08f4c7c32d4f71b54bda02913` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Polygon         | MATIC\_USDT       | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| Polygon         | MATIC\_USDC2      | `0x3c499c542cef5e3811e1192ce70d8cc03d5c3359` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| BNB Smart Chain | BSC\_USDT         | `0x55d398326f99059ff775485246999027b3197955` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| BNB Smart Chain | BSC\_USDC         | `0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d` | `0x3d963e23a9229d2acd25e9ffc358be1a35460ecc` |
| TRON            | TRON\_USDT        | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`         | `TEmDa9FY9YBCg1HPL41RUnJorXRRV3rX7A`         |

***

## **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**):

```json theme={null}
{
    "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)

```shell theme={null}
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 item fails validation, the batch fails and **every** item is marked `NotExecuted`.
* Under `Strict`, if the on-chain transfer step completes only partially, the batch status becomes `Failed`.
* For the validation-failure path, resubmission must include **all** items. For a partially completed sweep, inspect each item's `status` individually before deciding what to resubmit — do not assume every item needs resubmission.

Steps (validation-failure path):

1. Query items and identify those with `validation_status: ValidationFailed`.
2. Replace the addresses for those items (or fix other parameters causing failures).
3. Resubmit a new Bulk Send containing **all** items.

For a partially completed sweep, query each item's `status` individually and use that to decide which items (if any) to resubmit — do not default to resubmitting the full batch.

#### **`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.

```javascript theme={null}
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: for a validation failure, retry must include **all** items; for a partially completed sweep, inspect each item's `status` individually before deciding what to resubmit
  * 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
