API Limits and Constraints

The limits the LigueLead API enforces today, what happens when a request goes over one, and how to plan your integration around them.

🚦 Rate Limits

Current Limits

LimitValueScope
Request rate10,000 requests per secondThe API as a whole, not per token
BurstUp to 10,000 requests at onceThe API as a whole, not per token

A request over the limit is refused before it is processed, with 429 Too Many Requests. There is no separate limit per API token, per app or per endpoint.

Note: The API does not send rate-limit headers (X-RateLimit-* or similar). Rely on the 429 status instead of a remaining-requests counter.

Handling Rate Limits

A 429 means the request was not accepted, so it is safe to wait and send it again:

import random
import time

import requests

def post_with_rate_limit_retry(url, headers, data, max_retries=3):
    """POST, waiting and retrying only when the API answers 429."""

    for attempt in range(max_retries + 1):
        response = requests.post(url, headers=headers, json=data, timeout=35)

        if response.status_code != 429 or attempt == max_retries:
            return response

        delay = (2 ** attempt) + random.uniform(0, 0.5)
        print(f"⏱️  Rate limit hit, waiting {delay:.1f}s... (attempt {attempt + 1})")
        time.sleep(delay)

    return response

📱 Phone Number Limits

Recipients per Request

EndpointMaximum numbers per request
POST /v1/sms (SMS and SMS Flash)10,000
POST /v1/voice10,000
POST /v1/rcs10,000
POST /v1/voice-agent/call1,000

Every send needs at least one number. A longer list is refused with 422 — At most 10000 phone numbers are allowed per request (Maximum 1000 phone numbers allowed on AI calls). To reach more recipients, split them across several requests.

The request body is also limited to 250 KB (see Request Size below). A list of 10,000 numbers fits within it.

Phone Number Format

Accepted formats:

FormatExampleDescription
With country code5511999999999Country code 55 + area code + number
International+5511999999999Same, with a leading +
National11999999999Area code + number; 55 is added for you

Validation rules on POST /v1/sms, POST /v1/rcs and POST /v1/voice (each failure is a 422 whose field points to the entry, e.g. phones.3):

  • Only digits and the characters +, (, ) and - are accepted. Spaces and dots are refused: (11)99999-9999 is valid, (11) 99999-9999 is not.
  • Each number must have between 10 and 16 digits.
  • On POST /v1/voice, is_international: true skips these rules for numbers outside Brazil — see How to Send Voice Messages.

Best Practice: Send numbers with the country code (5511999999999 or +5511999999999). Without it, the length decides: 10 or 11 digits are a national number and get 55 in front — area code 55 included, so 55991234567 is sent as 5555991234567.

📦 Request Size

LimitValueWhen exceeded
JSON request body250 KB413 — Payload too large. Maximum allowed size is 250 KB.
Audio upload5 MB per file413 — File too large. Maximum allowed size is 5 MB.

RCS template endpoints accept inline media and have their own size rules — see How to Send RCS.

📁 File Upload Limits

Audio Files (Voice Messages)

SpecificationLimitNotes
File Size5 MB maximumLarger files are refused with 413
DurationNo specific limitThe file must contain decodable audio
Form fieldsfile and titleSent as multipart/form-data; both are required

Supported Audio Formats

FormatExtensionMIME typeRecommendation
WAV.wavaudio/wav or audio/wave⭐ Recommended — lossless, best audio quality
MP3.mp3audio/mpeg or audio/mp3Accepted, but lossy compression may reduce quality

Any other extension or MIME type (AAC, M4A, OGG, ...) is refused with 415 Unsupported Media Type.

⭐ Recommended format — WAV: Prefer uploading WAV files. MP3 uses lossy compression, so an MP3 upload may carry audible quality loss (compression artifacts) that WAV avoids. Every upload is normalized to the telephony standard (8 kHz, mono, 16‑bit PCM WAV) on our side, so starting from a lossless WAV preserves the best possible quality.

Important: The file's actual contents are checked against its extension: uploading a file whose bytes clearly belong to the other format (for example a WAV renamed to .mp3) is rejected with 415 Unsupported Media Type. A file that is not decodable audio is rejected while it is being processed, with 422 (or 415 for a WAV encoding that is not supported).

File Management

  • GET /v1/voice/uploads lists the 100 most recent audio files of the app, without pagination. Any file, older ones included, can still be read by its ID with GET /v1/voice/uploads/{id}.
  • There is no endpoint for deleting uploaded audio files, and uploaded files do not expire.

Audio Conversion Tips

# Convert an unsupported format to WAV in the telephony standard (8 kHz, mono, 16-bit)
ffmpeg -i input.m4a -ar 8000 -ac 1 -sample_fmt s16 output.wav

# Check duration and size before uploading
ffprobe -v quiet -show_entries format=duration,size input.wav

💬 Message Content Limits

SMS Messages

POST /v1/sms sends standard SMS, or SMS Flash with "is_flash": true. The same content rules apply to both:

SpecificationLimitNotes
Maximum Length1,600 charactersLonger messages are refused with 422
Up to 160 characters1 creditA single SMS
Over 160 characters1 credit per started block of 152 charactersCounted over the whole message: ceil(length / 152)
CharactersLetters, digits, spaces and common punctuationAccents are removed before sending (ç → c, ã → a), and characters outside this set — emoji included — are dropped

Credits are counted on the text actually sent, after accents and unsupported characters are removed.

SMS Billing Examples

Message: "Hello! Welcome to LigueLead API."   →    32 chars = 1 credit
Message: 160 characters exactly               →   160 chars = 1 credit
Message: 161 characters                       →   161 chars = 2 credits  (ceil(161 / 152))
Message: 304 characters                       →   304 chars = 2 credits  (ceil(304 / 152))
Message: 305 characters                       →   305 chars = 3 credits  (ceil(305 / 152))
Message: 1,600 characters (maximum)           → 1,600 chars = 11 credits (ceil(1600 / 152))

Character Count Calculator

import math
import re
import unicodedata

# Characters kept in the text that is sent; everything else is dropped.
_NOT_ALLOWED = re.compile(r"""[^A-Za-z0-9()@`"'=:/!?$%{}~^#_*,&+<>\\\[\]\s.-]""")

def normalize_sms(message: str) -> str:
    """The text actually sent: accents removed, then characters outside the allowed set dropped."""

    decomposed = unicodedata.normalize("NFD", message)
    without_accents = re.sub(r"[̀-ͯ]", "", decomposed)

    return _NOT_ALLOWED.sub("", without_accents)

def calculate_sms_credits(message: str) -> int:
    """Credits for an SMS, counted on the normalized text."""

    text = normalize_sms(message)
    length = len(text)

    if length == 0 or not text.strip():
        raise ValueError("Message cannot be empty")

    if length > 1600:
        raise ValueError("Message cannot exceed 1600 characters")

    if length <= 160:
        return 1

    return math.ceil(length / 152)

# Examples
print(repr(normalize_sms("Promoção café 🎉")))  # Output: 'Promocao cafe '
print(calculate_sms_credits("Hello World!"))   # Output: 1
print(calculate_sms_credits("A" * 160))        # Output: 1
print(calculate_sms_credits("A" * 161))        # Output: 2
print(calculate_sms_credits("A" * 305))        # Output: 3
print(calculate_sms_credits("é" * 161))        # Output: 2 (counted as "e" * 161)

The API also refuses, with 422, a message longer than 1,600 characters before normalization, so keep the text you submit within that limit as well.

RCS Messages

SpecificationLimit
Freeform message306 characters
Carousel2 to 10 cards

The template rules (body, buttons, media) are in How to Send RCS.

Titles and Names

FieldRule
Audio upload titleRequired
Voice send titleRequired
SMS send titleOptional
AI call title (POST /v1/voice-agent/call)Required, up to 200 characters
App nameRequired, 1–255 characters

⏱️ Timeouts

OperationLimitNotes
Any API request29 secondsA request still running after that is answered with 504 Gateway Timeout
Webhook delivery to your endpoint60 secondsThe attempt is abandoned and the notification is retried, up to 8 attempts over about 45 minutes — see How to Receive Webhooks

Send requests only queue the messages, so they normally answer well within the limit. Set your client timeout slightly above 29 seconds, so that a 504 is reported by the API instead of being cut by your own timeout.

🔄 Retry Strategies & Error Handling

Retry according to what the answer tells you about the request:

AnswerWas the send accepted?What to do
200 / 202YesKeep the returned campaign_id
429NoWait and retry (see above)
401, 413, 415, 422NoFix the request; retrying it unchanged fails again
5xx, 504 or a timeout on your sidePossiblyDo not resend blindly

⚠️ Important: Send endpoints have no idempotency key: every POST that is accepted creates a new campaign_id and is charged. A send that ended in a 5xx or a timeout may already have been accepted, in whole or in part, so repeating it can deliver — and charge — the same messages twice. Before resending, check what went out: for SMS, the webhooks and GET /v1/campaigns/{campaign_id}/recipients (see How to Check Campaign Status) report per recipient.

GET requests do not send anything and are always safe to retry.

📊 Concurrent Operations

There is no separate limit on concurrent requests per client: concurrent requests count towards the request rate above, which is shared by the whole API. Keep your own concurrency moderate, and handle 429 as described.

💾 Storage & Account Limits

Apps

ResourceLimitNotes
Active Apps per ClientNo limitHold as many apps as your integration needs
App Name1–255 charactersMust be unique among your active apps

The name is checked against active apps only, so a removed app frees its name for
reuse. A duplicate name is the only thing that answers 409 on creation. See
Managing Apps.

Audio File Storage

ResourceLimit
File Size5 MB per file
File RetentionFiles do not expire; there is no delete endpoint
ListingGET /v1/voice/uploads returns the 100 most recent files

🚀 Optimization Best Practices

1. Efficient Batch Operations

Good Practice: Combine multiple recipients in single requests

# ✅ Efficient: Single request with multiple recipients
payload = {
    "title": "Monthly Newsletter",
    "message": "Check out our latest updates!",
    "phones": ["5511999999999", "5521888888888", "5531777777777"]  # Up to 10,000 numbers
}

Avoid: Multiple single-recipient requests

# ❌ Inefficient: Multiple requests for individual recipients
for phone in phones:
    payload = {"message": "Hello", "phones": [phone]}  # One request per number

2. Audio File Reuse Strategy

import os

import requests

class AudioFileManager:
    """Upload each audio file once and reuse its ID across campaigns."""

    MIME_TYPES = {".wav": "audio/wav", ".mp3": "audio/mpeg"}

    def __init__(self):
        self.uploaded_files = {}  # Cache uploaded file IDs

    def upload_once_use_many(self, file_path, title, api_token, app_id):
        """Upload an audio file once and reuse it across campaigns."""

        # Check if already uploaded
        file_key = f"{title}_{hash(file_path)}"
        if file_key in self.uploaded_files:
            print(f"♻️  Reusing existing upload: {self.uploaded_files[file_key]}")
            return self.uploaded_files[file_key]

        # The part needs its MIME type: without it the upload is refused with 415
        extension = os.path.splitext(file_path)[1].lower()
        mime_type = self.MIME_TYPES[extension]

        with open(file_path, 'rb') as audio_file:
            files = {'file': (os.path.basename(file_path), audio_file, mime_type)}
            data = {'title': title}
            headers = {'api-token': api_token, 'app-id': app_id}

            response = requests.post(
                "https://api.liguelead.com.br/v1/voice/uploads",
                headers=headers,
                files=files,
                data=data
            )

            if response.status_code == 201:
                voice_id = response.json()['data']['id']
                self.uploaded_files[file_key] = voice_id
                print(f"✅ New upload successful: {voice_id}")
                return voice_id
            else:
                raise Exception(f"Upload failed: {response.text}")

# Usage
audio_manager = AudioFileManager()

# Upload once
voice_id = audio_manager.upload_once_use_many(
    "promo.mp3",
    "Black Friday Promotion",
    api_token,
    app_id
)

# Use in multiple campaigns
campaigns = [
    {"title": "VIP Customers", "phones": vip_numbers},
    {"title": "Regular Customers", "phones": regular_numbers},
    {"title": "New Prospects", "phones": prospect_numbers}
]

for campaign in campaigns:
    send_voice_message(voice_id, campaign["phones"], campaign["title"])

🆘 Support & Escalation

When to Contact Support

  • Receiving 429 regularly
  • File upload failures
  • Authentication issues
  • Unexpected API behavior
  • Volume that you expect to approach the request rate above

What to Include in Support Requests

  1. Account Information

    • API token (last 4 characters only)
    • App ID
    • Account email
  2. Technical Details

    • The campaign_id of the affected send, when there is one
    • Current usage patterns
    • Expected volume increase
    • Specific limits being hit
    • Error messages or response codes
  3. Business Context

    • Use case description
    • Timeline requirements
    • Integration complexity

Contact Methods


Did this page help you?