Batch Operations

This guide shows you how to send high-volume campaigns and import contacts in bulk while staying inside Sent's API limits.

Overview

When dealing with large volumes:

  • Batch message sending - Send to up to 1000 recipients in one request
  • Bulk contact import - Import thousands of contacts efficiently
  • Rate limit management - Stay within API limits
  • Queue-based processing - Process large jobs asynchronously

Large-Scale Sending Flow

Batch Message Sending

Multiple Recipients

Send the same message to up to 1000 recipients in one request:

curl -X POST "https://api.sent.dm/v3/messages" \
  -H "x-api-key: $SENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [
      "+1234567890",
      "+1987654321",
      "+1555555555"
    ],
    "template": {
      "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
      "parameters": {
        "announcement": "New feature launched!"
      }
    },
    "channel": ["sms", "whatsapp", "rcs"]
  }'
const response = await client.messages.send({
  to: [
    '+1234567890',
    '+1987654321',
    '+1555555555'
    // Up to 1000 recipients
  ],
  template: {
    id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
    parameters: {
      announcement: 'New feature launched!'
    }
  }
});

// Track individual message IDs
const messageIds = response.data.recipients.map(r => r.message_id);
console.log(`Sent ${messageIds.length} messages`);
response = client.messages.send(
    to=[
        "+1234567890",
        "+1987654321",
        "+1555555555"
        # Up to 1000 recipients
    ],
    template={
        "id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        "parameters": {
            "announcement": "New feature launched!"
        }
    }
)

message_ids = [r.message_id for r in response.data.recipients]
print(f"Sent {len(message_ids)} messages")
response, err := client.Messages.Send(context.Background(), sentdm.MessageSendParams{
    To: []string{
        "+1234567890",
        "+1987654321",
        "+1555555555",
        // Up to 1000 recipients
    },
    Channel: []string{"sms", "whatsapp", "rcs"},
    Template: sentdm.MessageSendParamsTemplate{
        ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
        Parameters: map[string]interface{}{
            "announcement": "New feature launched!",
        },
    },
})

// Track individual message IDs
messageIDs := make([]string, len(response.Data.Recipients))
for i, r := range response.Data.Recipients {
    messageIDs[i] = r.MessageID
}
fmt.Printf("Sent %d messages\n", len(messageIDs))
MessageSendParams params = MessageSendParams.builder()
    .addTo("+1234567890")
    .addTo("+1987654321")
    .addTo("+1555555555")
    .addChannel("sms")
    .addChannel("whatsapp")
    .addChannel("rcs")
    .template(MessageSendParams.Template.builder()
        .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
        .parameters(MessageSendParams.Template.Parameters.builder()
            .putAdditionalProperty("announcement", JsonValue.from("New feature launched!"))
            .build())
        .build())
    .build();

var response = client.messages().send(params);

// Track individual message IDs
List<String> messageIds = response.data().recipients().stream()
    .map(r -> r.messageId())
    .toList();
System.out.println("Sent " + messageIds.size() + " messages");
MessageSendParams parameters = new()
{
    To = new List<string> {
        "+1234567890",
        "+1987654321",
        "+1555555555"
        // Up to 1000 recipients
    },
    Channels = new List<string> { "sms", "whatsapp", "rcs" },
    Template = new MessageSendParamsTemplate
    {
        Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
        Parameters = new Dictionary<string, string>
        {
            { "announcement", "New feature launched!" }
        }
    }
};

var response = await client.Messages.Send(parameters);

// Track individual message IDs
var messageIds = response.Data.Recipients.Select(r => r.MessageId).ToList();
Console.WriteLine($"Sent {messageIds.Count} messages");
$result = $client->messages->send(
    to: [
        '+1234567890',
        '+1987654321',
        '+1555555555'
        // Up to 1000 recipients
    ],
    template: [
        'id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8',
        'parameters' => [
            'announcement' => 'New feature launched!'
        ]
    ],
    channels: ['sms', 'whatsapp', 'rcs']
);

// Track individual message IDs
$message_ids = array_map(fn($r) => $r->message_id, $result->data->recipients);
echo "Sent " . count($message_ids) . " messages\n";
result = sent_dm.messages.send_(
  to: [
    "+1234567890",
    "+1987654321",
    "+1555555555"
    # Up to 1000 recipients
  ],
  template: {
    id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    parameters: {
      announcement: "New feature launched!"
    }
  },
  channels: ["sms", "whatsapp", "rcs"]
)

# Track individual message IDs
message_ids = result.data.recipients.map(&:message_id)
puts "Sent #{message_ids.length} messages"

Batch Response

{
  "success": true,
  "data": {
    "status": "QUEUED",
    "template_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    "template_name": "product_announcement",
    "recipients": [
      { "message_id": "8ba7b830-9dad-11d1-80b4-00c04fd430c8", "to": "+1234567890", "channel": "sms" },
      { "message_id": "8ba7b831-9dad-11d1-80b4-00c04fd430c8", "to": "+1987654321", "channel": "whatsapp" },
      { "message_id": "8ba7b832-9dad-11d1-80b4-00c04fd430c8", "to": "+1555555555", "channel": "sms" }
    ]
  },
  "error": null,
  "meta": {
    "request_id": "req_batch_001",
    "timestamp": "2026-03-04T11:28:25.2096416+00:00",
    "version": "v3"
  }
}

Batch requests count against rate limits per request (not per recipient). Each POST /v3/messages call counts as one request toward the 200 req/min limit, regardless of how many recipients are included.

Large-Scale Sending (10,000+ Recipients)

For campaigns with tens of thousands of recipients, process the list in slices of 1000 with a delay between batches:

// batchProcessor.ts
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

async function sendCampaign(recipients: string[], templateId: string) {
  const BATCH_SIZE = 1000; // Maximum recipients per request
  const DELAY_BETWEEN_BATCHES = 1000; // 1 second

  const results = {
    sent: 0,
    failed: 0,
    messageIds: []
  };

  // Process in batches
  for (let i = 0; i < recipients.length; i += BATCH_SIZE) {
    const batch = recipients.slice(i, i + BATCH_SIZE);

    try {
      const response = await client.messages.send({
        to: batch,
        template: { id: templateId }
      });

      // The 202 response confirms acceptance; track delivery via webhooks
      results.sent += response.data.recipients.length;
      results.messageIds.push(...response.data.recipients.map(r => r.message_id));

      console.log(`Batch ${i / BATCH_SIZE + 1} complete: ${batch.length} messages`);

      // Rate limiting delay (except for last batch)
      if (i + BATCH_SIZE < recipients.length) {
        await sleep(DELAY_BETWEEN_BATCHES);
      }
    } catch (error) {
      console.error(`Batch ${i / BATCH_SIZE + 1} failed:`, error);
      results.failed += batch.length;
    }
  }

  return results;
}

// Usage
const recipients = await getAllCustomerPhoneNumbers(); // 5000 numbers
const results = await sendCampaign(recipients, '7ba7b820-9dad-11d1-80b4-00c04fd430c8');
console.log(`Campaign complete: ${results.sent} sent, ${results.failed} failed`);

Bulk Contact Import

CSV Import

Format your CSV file:

phone_number
+1234567890
+1987654321
+1555555555

Import script:

import { parse } from 'csv-parse';
import fs from 'fs';

const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

async function importContacts(csvPath: string) {
  const parser = fs.createReadStream(csvPath).pipe(parse({
    columns: true,
    skip_empty_lines: true
  }));

  const results = { created: 0, failed: 0, errors: [] };
  const BATCH_SIZE = 50;
  let batch = [];

  for await (const record of parser) {
    batch.push({
      phoneNumber: record.phone_number
    });

    if (batch.length >= BATCH_SIZE) {
      const result = await processBatch(batch);
      results.created += result.created;
      results.failed += result.failed;
      results.errors.push(...result.errors);
      batch = [];

      // Rate limit protection
      await sleep(100);
    }
  }

  // Process remaining
  if (batch.length > 0) {
    const result = await processBatch(batch);
    results.created += result.created;
    results.failed += result.failed;
  }

  return results;
}

async function processBatch(batch: any[]) {
  const results = { created: 0, failed: 0, errors: [] };

  await Promise.all(batch.map(async (contact) => {
    try {
      await client.contacts.create(contact);
      results.created++;
    } catch (error) {
      results.failed++;
      results.errors.push({ contact, error: error.message });
    }
  }));

  return results;
}

Rate Limit Management

Understanding Limits

EndpointLimitWindow
POST /v3/messages200 requests1 minute
POST /v3/contacts200 requests1 minute
GET /v3/*200 requests1 minute

Limits count per request, not per recipient, and all API keys on an account share one pool. A stricter tier of 10 requests per minute applies only to two sensitive webhook endpoints, POST /v3/webhooks/{id}/rotate-secret and POST /v3/webhooks/{id}/test, neither of which is involved in batch sending. Refer to the Rate Limits reference for the full per-endpoint table and rate limit headers.

Staying Under the Limit

Space your batch requests so total throughput stays below 200 requests per minute; the one-second delay between batches in the Large-Scale Sending loop keeps you at a safe 60 requests per minute. If you need finer-grained control, refer to the Rate Limits reference for ready-made throttle and backoff implementations in TypeScript, Python, and Go.

Handling Rate Limit Errors

If a request returns 429, honor the Retry-After header before retrying:

const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));

async function sendWithBackoff(phoneNumber: string, templateId: string, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await client.messages.send({ to: [phoneNumber], template: { id: templateId } });
    } catch (error) {
      if (error.status === 429) {
        // Get retry-after header or use exponential backoff
        const retryAfter = error.headers['retry-after'] || Math.pow(2, attempt);
        console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
        await sleep(retryAfter * 1000);
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

Queue-Based Processing

For sustained high volume, run sends through a persistent job queue (for example BullMQ, Sidekiq, or Celery) instead of a single in-process loop. The queue and worker mechanics belong to your infrastructure; the Sent-specific requirements are:

  • One job per batch - Group up to 1000 recipients per job so each job makes a single POST /v3/messages call.
  • Bounded concurrency - Cap workers so combined throughput stays under 200 requests per minute.
  • Idempotent jobs - Set an idempotency key per job so queue retries don't double-send; see Retry Safely with Idempotency Keys.
  • Persist message IDs - Store the message_id values from each 202 response so webhook events can be matched back to jobs.
  • Watch queue depth - Alert when jobs accumulate faster than workers drain them.

Reuse a single SDK client instance across jobs so HTTP connections are pooled.

Monitoring Bulk Operations

Log progress per batch from the counts in each 202 response, as the Large-Scale Sending loop does. For delivery outcomes, subscribe to the webhook events message.delivered, message.failed, message.filtered, and message.blocked instead of polling GET /v3/messages/{id} for every message; see Status Tracking.

Best Practices

Handle Partial Failures

A 202 response confirms acceptance, not delivery. Validation is all-or-nothing (a 400 rejects the whole request), so every recipient in an accepted batch gets a message_id, but individual messages can still end FAILED, FILTERED (recipient opted out), or BLOCKED (insufficient balance) asynchronously. Persist the accepted IDs and reconcile them against webhook events:

const recipients = ['+14155551234', '+14155555678'];

const response = await client.messages.send({
  to: recipients,
  template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' }
});

// Persist each accepted message so your webhook handler can mark it
// DELIVERED, FAILED, FILTERED, or BLOCKED as events arrive
const accepted = response.data.recipients.map((r) => ({
  messageId: r.message_id,
  to: r.to,
  status: 'PENDING'
}));

console.log(`Accepted ${accepted.length} messages`);

Retry Safely with Idempotency Keys

Give every batch a deterministic idempotency key so a retried request (after a timeout, 429, or worker crash) replays the original response instead of double-sending:

const recipients = ['+14155551234', '+14155555678'];
const campaignId = 'spring_launch';
const batchNumber = 1;

const response = await client.messages.send(
  {
    to: recipients,
    template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' }
  },
  { idempotencyKey: `campaign_${campaignId}_${batchNumber}` }
);

Refer to the Idempotency reference for key format and replay behavior.

Start Small

Before launching a full campaign, run the batch pipeline against a small slice, for example sendCampaign(recipients.slice(0, 10), templateId), and confirm delivery through webhooks before committing the full list.


On this page