How to Integrate with the Amplitude API: 2026 Engineering Guide for B2B SaaS
A definitive engineering guide to the Amplitude API in 2026. Learn how to architect reliable event ingestion, handle 429 rate limits, and prevent data loss.
If you are an engineering lead or product manager at a B2B SaaS company trying to push event data into Amplitude, you need to know three things immediately: which API endpoint to use, how to avoid getting throttled by Amplitude's per-user rate limits, and whether building a custom integration is worth the engineering time. If your B2B SaaS application cannot reliably sync customer data and events to Amplitude, your enterprise buyers will find a competitor who can.
Engineering teams tasked with building a product analytics integration often assume it is a simple matter of firing HTTP POST requests to an endpoint. That assumption breaks down in production the moment your application hits Amplitude's strict concurrency limits, drops critical event payloads, or fails to handle property updates at scale. This guide covers all the architectural specifics that Amplitude's own documentation buries across multiple pages.
The State of Product Analytics Integrations in 2026
Enterprise buyers do not purchase isolated software. They purchase nodes in a massive, interconnected graph of data. When a customer evaluates your SaaS product, they expect your platform to natively push usage metrics, feature flags, and conversion events directly into their central analytics stack. As your product grows and your customers demand tighter analytics integrations—what we classify as customer-facing integrations—the pressure to get event data flowing reliably only increases.
The global B2B SaaS market is projected to grow from $634 billion in 2026 to over $4.4 trillion by 2034 at a CAGR of 27.54%, according to Fortune Business Insights. This rapid expansion is driven by cloud adoption and the demand for AI-driven, operationalized analytics. Customers no longer tolerate manual CSV exports or brittle third-party middleware to move data from your application into Amplitude. If your integration drops events during traffic spikes, your customers' product managers and data scientists will immediately notice the discrepancies in their funnels and cohort analyses.
Amplitude HTTP V2 vs. Batch Event Upload API: Which Should You Use?
Amplitude provides two primary ingestion endpoints for event data. Choosing the wrong one for your architecture will result in immediate rate limiting or unacceptable latency. The HTTP V2 API is for real-time, low-latency event streaming. The Batch API is for high-volume backfills and server-side ingestion at scale.
Here is a breakdown of when to use each:
| Criteria | HTTP V2 API | Batch Event Upload API |
|---|---|---|
| Endpoint | https://api2.amplitude.com/2/httpapi |
https://api2.amplitude.com/batch |
| Throttle per user/device | 30 events/sec | 1,000 events/sec |
| Max payload size | 1 MB | 20 MB |
| Event ordering | Preserved per device_id |
Not guaranteed |
| Best for | Real-time user actions | Backfills, batch imports, server-side pipelines |
| EU endpoint | api.eu.amplitude.com/2/httpapi |
api.eu.amplitude.com/batch |
The HTTP V2 API
The Amplitude HTTP V2 API is designed for real-time event ingestion. It processes events in the exact order they are received for a given device_id, which makes it the right choice for tracking live user journeys where sequence matters—think funnel analysis or session replay correlation. If your SaaS application needs to trigger immediate downstream actions in a customer's stack, like firing an in-app message based on a specific user action, this is the endpoint to use.
The Batch Event Upload API
For high-volume data ingestion, the Amplitude Batch Event Upload API is mandatory. If your SaaS platform processes thousands of transactions, background jobs, or bulk imports on behalf of your users, attempting to push these through the HTTP V2 API will instantly trigger HTTP 429 Too Many Requests errors. The Batch API buffers data longer and is designed to absorb bursts, which makes it the obvious pick for nightly syncs, historical imports, or fan-out from webhook pipelines.
Architectural Tip: A common mistake is starting with the HTTP V2 API for everything, hitting the 30 events/sec per-user ceiling during load testing, and panic-migrating to the Batch API mid-sprint. Most B2B SaaS platforms should default to the Batch Event Upload API for server-to-server integrations. The slight delay in data availability is worth the massive reduction in network overhead and rate limit complexity.
Authentication and Security for the Amplitude API
Amplitude uses different authentication schemes depending on which API surface you are interacting with. Mixing these up will result in HTTP 401 Unauthorized errors.
Ingestion APIs (HTTP V2 and Batch)
For endpoints that write data into Amplitude, authentication is handled via an API Key included directly in the JSON request body as api_key—not as a header, and not as a query parameter. Most modern analytics APIs use Bearer tokens or header-based auth. Amplitude's approach is simpler but means your API key travels directly in the request payload.
{
"api_key": "YOUR_AMPLITUDE_API_KEY",
"events": [
{
"user_id": "user-9f3a2b",
"event_type": "feature_activated",
"time": 1714003200000,
"event_properties": {
"feature_name": "export_dashboard",
"plan": "enterprise"
},
"insert_id": "evt-9f3a2b-1714003200000"
}
]
}Never expose your Amplitude API key in client-side code. The event ingestion API keys are write-only, but a leaked key lets anyone write garbage events into your project. For B2B SaaS integrations, you should exclusively use server-side ingestion.
Dashboard REST API
If you need to query data out of Amplitude (for example, to pull cohort definitions back into your SaaS product), you must use the Dashboard REST API. This API requires Basic Authentication. You need to Base64-encode your API key and secret key as API_KEY:SECRET_KEY and pass it in the Authorization: Basic header. This distinction trips engineering teams up regularly.
Always store the Secret Key in a secure, encrypted vault. The API Key is generally safe to expose for client-side tracking, but the Secret Key grants full read/write access to the project's historical data.
Amplitude Endpoint Reference and Example Payloads
Beyond the two ingestion endpoints, here is the full URL surface most B2B SaaS teams will actually touch when integrating Amplitude analytics API with a SaaS product:
| Purpose | US endpoint | EU endpoint |
|---|---|---|
| HTTP V2 ingestion | https://api2.amplitude.com/2/httpapi |
https://api.eu.amplitude.com/2/httpapi |
| Batch ingestion | https://api2.amplitude.com/batch |
https://api.eu.amplitude.com/batch |
| Identify API | https://api2.amplitude.com/identify |
https://api.eu.amplitude.com/identify |
| Group Identify | https://api2.amplitude.com/groupidentify |
https://api.eu.amplitude.com/groupidentify |
| Attribution | https://api2.amplitude.com/attribution |
https://api.eu.amplitude.com/attribution |
| Dashboard REST | https://amplitude.com/api/2/ |
https://analytics.eu.amplitude.com/api/2/ |
| Cohort download | https://amplitude.com/api/5/cohorts/request/{cohort_id} |
https://analytics.eu.amplitude.com/api/5/cohorts/request/{cohort_id} |
A complete HTTP V2 request
A production-ready HTTP V2 call includes Content-Type: application/json, an explicit min_id_length option if you use short internal IDs, and an insert_id on every event:
POST /2/httpapi HTTP/1.1
Host: api2.amplitude.com
Content-Type: application/json
Accept: */*
{
"api_key": "YOUR_AMPLITUDE_API_KEY",
"events": [
{
"user_id": "customer-4402",
"device_id": "web-2b7d1e9c",
"event_type": "dashboard_exported",
"time": 1714003200000,
"event_properties": {
"format": "csv",
"row_count": 12483
},
"user_properties": {
"$set": { "plan": "enterprise" }
},
"insert_id": "exp-2b7d1e9c-1714003200000"
}
],
"options": { "min_id_length": 5 }
}A successful 200 response is small and self-describing:
{
"code": 200,
"events_ingested": 1,
"payload_size_bytes": 312,
"server_upload_time": 1714003201234
}A 429 response you actually need to handle
Amplitude's 429 body is the important artifact. It tells you exactly which users and devices to pause - not the entire request:
{
"code": 429,
"error": "Too many requests for some devices and users",
"eps_threshold": 30,
"throttled_devices": { "web-2b7d1e9c": 42 },
"throttled_users": { "customer-4402": 78 },
"throttled_events": [3, 7, 12]
}throttled_events is an index into the events array you sent. Amplitude accepts the un-throttled events in the same request, so a naive "retry the whole batch" strategy will double-count the accepted ones unless every event has an insert_id.
A Batch API request with mixed properties
The Batch API accepts the same event schema. The difference is you can pack up to 20 MB of events per request and Amplitude tolerates higher per-identity throughput:
POST /batch HTTP/1.1
Host: api2.amplitude.com
Content-Type: application/json
{
"api_key": "YOUR_AMPLITUDE_API_KEY",
"events": [
{
"user_id": "customer-4402",
"event_type": "invoice.paid",
"time": 1714003200000,
"event_properties": {
"amount_usd": 4900,
"plan": "enterprise",
"invoice_id": "in_1PxYz9"
},
"user_properties": {
"$set": { "last_paid_at": 1714003200000 },
"$add": { "lifetime_revenue_usd": 4900 }
},
"insert_id": "invoice-in_1PxYz9-paid"
},
{
"user_id": "customer-4402",
"event_type": "seat_added",
"time": 1714003260000,
"event_properties": { "seat_count": 12 },
"$skip_user_properties_sync": true,
"insert_id": "seat-add-2b7d1e"
}
]
}Notice $skip_user_properties_sync on the second event. If you are firing many events per user but only need property updates on some of them, this flag protects you from the 1,800 property-updates-per-hour limit covered below.
Navigating Amplitude Rate Limits and 429 Errors
Handling rate limits is the single most complex part of building an Amplitude integration. Amplitude's infrastructure aggressively protects itself from misconfigured clients. They enforce three distinct rate limits, and conflating them is the fastest way to lose events in production:
- Per-user/device event throughput (HTTP V2): 30 events per second, averaged over a 30-second window. Exceeding this returns a
429withthrottled_usersandthrottled_devicesmaps in the response body. - Per-user/device event throughput (Batch API): 1,000 events per second, with a daily cap of 500,000 events per user/device.
- User property update limit: 1,800 updates per hour per user (by Amplitude ID). This applies across all APIs—HTTP V2, Batch, and the Identify API.
That third limit is the quiet killer. If your application is syncing user properties on every event (a common pattern when you use user_properties in the event payload), you can silently lose property updates without any error response. Amplitude keeps ingesting the standard events—it just stops applying the property changes entirely, leading to a silent desync between your database and the customer's Amplitude instance.
The practical defense is a per-user property-update counter. Increment a Redis counter keyed by user_id:hour_bucket every time you send a payload with user_properties. When the counter approaches 1,500, switch subsequent events to $skip_user_properties_sync: true for that user until the hour rolls over. This leaves 300 updates of headroom for critical properties.
Architecting Exponential Backoff
When you receive an HTTP 429 error, you must not immediately retry the request. You must implement exponential backoff with jitter to prevent the "thundering herd" problem. Furthermore, the response body tells you exactly which users and devices were throttled.
Here is a practical, production-ready backoff implementation:
import time
import requests
import random
def send_to_amplitude(payload, max_retries=5):
url = "https://api2.amplitude.com/2/httpapi"
for attempt in range(max_retries):
response = requests.post(url, json=payload)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
error_body = response.json()
throttled_users = error_body.get("throttled_users", {})
throttled_devices = error_body.get("throttled_devices", {})
# Log which users/devices are hot
for uid, rate in throttled_users.items():
print(f"User {uid} throttled at {rate} eps")
# Calculate exponential backoff: base_delay * 2^attempt
# Amplitude recommends a 30s pause for throttled identities
exponential_delay = 30 * (2 ** attempt)
# Add jitter to prevent synchronized retries across concurrent workers
jitter = random.uniform(0, 5)
# Cap the delay at the maximum allowed time (e.g., 5 minutes)
wait = min(exponential_delay + jitter, 300)
time.sleep(wait)
continue
if response.status_code in (500, 502, 504):
# Server error - retry with insert_id for dedup
time.sleep(2 ** attempt)
continue
# 400, 413, etc. - don't retry, fix the payload
response.raise_for_status()
raise Exception("Max retries exceeded for Amplitude ingestion")The key detail from Amplitude's documentation: when you get a 429 on the HTTP V2 API, you should pause sending events for that specific user or device for 30 seconds before retrying. This is not a global backoff—it is per-identity. If you are sending events for 10,000 users and only one is throttled, you should only pause that one user's events.
Concrete Backoff Schedule
Here is the exact schedule the code above produces, expressed as a table you can hand to your on-call engineers:
| Attempt | Base pause (per identity) | With jitter | Cumulative wait |
|---|---|---|---|
| 1 | 30 s | 30-35 s | ~30 s |
| 2 | 60 s | 60-65 s | ~90 s |
| 3 | 120 s | 120-125 s | ~210 s |
| 4 | 240 s | 240-245 s | ~450 s |
| 5 | 300 s (capped) | 300-305 s | ~750 s |
After the 5th attempt, route the payload to a dead-letter queue rather than continuing to hammer the API. A user who is still 429ing after ~12 minutes of backoff either has a genuine misconfiguration (an integration replay loop, a runaway client SDK) or is being flagged by Amplitude's abuse detection, and blind retries will not fix either.
Hold the pause state per identity in a fast key-value store with a TTL:
# When a 429 comes back for user_id
redis.setex(f"amp:pause:user:{user_id}", 30, "1")
# Before sending each event
if redis.exists(f"amp:pause:user:{event['user_id']}"):
requeue_with_delay(event, seconds=5)
continueThis keeps hot users out of the send loop without stalling healthy traffic for other identities.
Event Deduplication with insert_id
Amplitude deduplicates events using a combination of device_id and insert_id within a rolling 7-day window. If you retry a failed request (especially after a 5xx server error), you must always include an insert_id to prevent duplicate events. Amplitude recommends generating a UUID or composing one from device_id + user_id + event_type + event_id + time.
This is not optional in production. Without insert_id, a single network timeout followed by a retry can double-count a revenue event or artificially inflate your activation metrics.
Batch Sizing and Payload Limits
The 20 MB Batch API limit sounds generous, but hitting it is easier than most engineers expect. An Amplitude event with 20 to 30 event properties averages 500 bytes to 1.5 KB serialized. That gives a theoretical ceiling of roughly 40,000 events per batch, but you should never target that number - Content-Type: application/json overhead, gzip variability, and the occasional event with a large event_properties blob will push you over.
Recommended defaults for a Batch API worker:
| Setting | Recommended default | Rationale |
|---|---|---|
| Max events per batch | 2,000 | Keeps payload under ~3 MB even with fat properties |
| Max payload size before flush | 4 MB | Well below the 20 MB hard limit, gives 5x headroom |
| Max wait before flush | 5 seconds | Bounds ingestion latency for slow-moving customers |
Concurrent workers per user_id |
1 | Preserves event ordering per identity |
| Concurrent workers overall | 20-50 | Tune based on total events/sec across your tenant base |
| Gzip request bodies | Yes | Cuts wire size 5-10x for repetitive event schemas |
A batching worker in pseudocode:
BATCH_MAX_EVENTS = 2000
BATCH_MAX_BYTES = 4 * 1024 * 1024 # 4 MB
BATCH_MAX_WAIT_MS = 5000
def batch_loop(queue):
buffer, buffer_bytes = [], 0
flush_deadline = now_ms() + BATCH_MAX_WAIT_MS
while True:
event = queue.poll(timeout_ms=100)
if event:
event_bytes = len(json.dumps(event))
# Flush before adding if this event would push us over
if buffer_bytes + event_bytes > BATCH_MAX_BYTES:
flush(buffer)
buffer, buffer_bytes = [], 0
flush_deadline = now_ms() + BATCH_MAX_WAIT_MS
buffer.append(event)
buffer_bytes += event_bytes
if len(buffer) >= BATCH_MAX_EVENTS or now_ms() >= flush_deadline:
if buffer:
flush(buffer)
buffer, buffer_bytes = [], 0
flush_deadline = now_ms() + BATCH_MAX_WAIT_MSIf a single event exceeds 4 MB on its own (rare, but it happens with events that embed large user_properties documents), route it directly to the claim-check pipeline described below rather than trying to squeeze it into a batch.
For a deeper look at rate limit handling across multiple third-party APIs simultaneously, see our guide on best practices for handling API rate limits and retries.
Architecting a Reliable Webhook-to-Amplitude Pipeline
The typical B2B SaaS pattern looks like this: your application fires internal events (a ticket closing, a payment succeeding, or a user role changing), and you need those events to land in a customer's Amplitude instance reliably. Building this as a direct synchronous call from your app's main thread to Amplitude is a severe anti-pattern for three reasons:
- Amplitude downtime blocks your app. If the Amplitude API returns a 503 and you are making synchronous calls, your user-facing request hangs.
- Burst traffic exceeds rate limits. A product launch or marketing campaign can easily spike your event volume past the 30 events/sec per-user limit.
- No replay capability. When Amplitude has an outage, you lose events unless you have a durable buffer.
To ensure zero data loss, you must architect a durable webhook fan-out system.
flowchart LR
A[SaaS App Core] -->|Fires Event| B[Internal Event Bus]
B -->|Routes to| C[Webhook Ingestion Service]
C -->|Validates & Enqueues| D[(Durable Queue)]
D -->|Pulls Batch by user_id| E[Worker Pool]
E -->|Transforms Payload| F{Amplitude Batch API}
F -- HTTP 200 --> G[Mark Complete]
F -- HTTP 429 --> H[Calculate Backoff & Re-queue]
F -- HTTP 500 --> HKey Components for Reliability
- Decoupled Ingestion: Your core application should drop events into an internal message broker or event bus and immediately return a success response to the user.
- Durable Queuing & Partitioning: Workers pull from a durable queue. If a worker crashes mid-flight, the message remains in the queue to be processed by another node. Crucially, you must partition your queue by
user_idordevice_id. Because Amplitude's throttling is per-identity, if one spammy user triggers a 429, you want that to only block that user's partition—not your entire pipeline. - Payload Transformation: Amplitude requires specific JSON schemas. Your worker must map your internal data model (e.g.,
ticket.status = closed) to the customer's desired Amplitude event taxonomy. - Dead-Letter Queues (DLQ): If an event fails to sync after maximum retries (e.g., due to a malformed payload or a revoked API key), it must be routed to a DLQ for engineering review, rather than infinitely blocking the queue.
For patterns on building reliable webhook fan-out systems, including signature verification and delivery guarantees, see our production lessons in designing reliable webhooks.
Webhook Verification and a Sample Declarative Template
When your SaaS application is receiving webhooks from upstream systems (Stripe, HubSpot, Segment, GitHub, etc.) and fanning them out to a customer's Amplitude instance, you cannot trust the inbound payload until you have verified its signature. Every upstream provider signs webhooks differently, but the verification pattern is universal: extract a signature from a header, recompute an HMAC over the raw request body using a shared secret, and compare the two using a timing-safe equality check.
A minimal HMAC-SHA256 verifier in Node.js:
import crypto from 'crypto';
export function verifyWebhookSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(signatureHeader.replace(/^sha256=/, ''), 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}The gotcha most teams miss: verification must run against the raw request body, not the parsed JSON. Any middleware that parses and re-serializes the body will change whitespace or key ordering and break the signature. Reach for the raw buffer before your JSON parser sees it.
A declarative FreeMarker-style template
Hardcoding one verifier per provider gets ugly around integration #5. A cleaner pattern is a declarative config with placeholder substitution, where each integration describes which header to read, which algorithm to use, and how to construct the signed payload. A sample template for a webhook subscription:
# webhook-verification.ftl
verification:
algorithm: hmac-sha256
signature_header: "${headers['x-hub-signature-256']}"
signature_prefix: "sha256="
signed_payload_template: "${body.raw}"
secret: "${subscription.secret}"
encoding: hex
timing_safe: true
dispatch:
target: amplitude.batch
transform: |
{
"user_id": "${body.data.customer.id}",
"event_type": "${body.type}",
"time": ${body.created * 1000},
"event_properties": ${body.data.object},
"insert_id": "${body.id}"
}At runtime, ${headers ['x-hub-signature-256']}, ${body.raw}, and ${subscription.secret} are substituted from the incoming request. The verification engine computes the expected signature and compares it timing-safely. The dispatch.transform block then rewrites the upstream payload into an Amplitude-shaped event for the Batch API.
Truto uses this declarative pattern internally: the subscription secret is encrypted at rest, signature comparison is timing-safe, and placeholders like {{headers.x-hub-signature}} or {{body.token}} in the verification config are substituted from the payload at runtime. Adding support for a new webhook provider becomes a config change instead of new signature-parsing code.
Non-negotiables for inbound webhooks feeding Amplitude
Before an inbound event is trusted enough to become an Amplitude ingestion, three things need to be true:
- Idempotency. Store the upstream event ID (or a hash of the raw body) and drop duplicates. Providers redeliver on network hiccups; some send
POSTandGETvariants during URL validation. - Payload durability. Persist the raw payload to object storage before you acknowledge the webhook. If downstream ingestion into Amplitude fails, you can replay from the raw stored payload rather than losing the event.
- Fast 2xx acknowledgement. Return 200 within 2 to 3 seconds. Long processing belongs in a worker - upstreams retry aggressively (and sometimes exponentially) when they see timeouts.
Claim-Check Pattern for Large Cohort Syncs
When you need to sync a large cohort from your SaaS product into Amplitude - a customer onboarding 50,000 users, a nightly recomputation of "power users" for a Fortune 500 tenant, or a historical backfill after adding a new event type - inline payloads become impractical. You will hit queue message size limits, run into memory pressure in workers, and create coordination problems if multiple workers race the same batch.
The pattern to use is the claim-check pattern: put the actual data in object storage, put a lightweight reference in the queue, and let workers fetch the payload on demand.
sequenceDiagram
participant App as SaaS App
participant Store as Object Storage
participant Queue as Durable Queue
participant Worker as Amplitude Worker
participant Amp as "Amplitude Batch API"
App->>Store: PUT cohort-2026-04-25.jsonl (200 MB)
Store-->>App: object_key
App->>Queue: Enqueue { object_key, cohort_id, total_events }
Queue-->>Worker: Deliver claim check
Worker->>Store: GET object_key (streamed)
Store-->>Worker: JSONL stream
loop For each 2000-event chunk
Worker->>Amp: POST /batch
Amp-->>Worker: 200 OK
Worker->>Queue: Update checkpoint
end
Worker->>Queue: ACKWhy this matters specifically for Amplitude:
- Queue message size limits. Most durable queue systems cap message payloads at 256 KB to 1 MB. A single cohort with even 5,000 events can easily exceed this.
- Retry cost. If a worker crashes at event 40,000 of 50,000 and the payload was inline in the queue message, the entire 50,000 events must be redelivered. With a claim-check, the worker checkpoints its byte offset in the object and resumes.
- Rate-limit coordination. Because Amplitude throttles per-identity, streaming a large cohort through a small pool of workers with a controlled events/sec ceiling is safer than fanning out and racing the API.
A typical claim-check message payload:
{
"type": "amplitude.cohort_sync",
"object_key": "payloads/cohort-2026-04-25-9f3a2b.jsonl",
"cohort_id": "cohort-9f3a2b",
"total_events": 48291,
"customer_id": "acme-corp",
"target_endpoint": "https://api2.amplitude.com/batch",
"created_at": 1714003200000,
"checkpoint_offset": 0
}The worker reads the object as a stream (never into memory), batches events into 2,000-event or 4 MB chunks, POSTs each chunk to the Batch API, updates checkpoint_offset after each successful batch, and ACKs the message when the cohort is fully drained. If the worker dies mid-sync, another worker picks up the message and resumes from the last checkpoint - no duplicate events (thanks to deterministic insert_ids) and no data loss.
Truto uses the same pattern for large inbound webhook payloads: raw payloads are persisted to object storage before the internal queue message is enqueued, so each queue message carries only the storage reference. This keeps queue messages tiny, decouples payload size from queue limits, and makes replay after downstream failures a one-line operation.
Amplitude API Gotchas That Will Bite You in Production
Beyond rate limits and architecture, there are several strict payload constraints that Amplitude's documentation mentions but does not emphasize enough. These will cause immediate 400 Bad Request errors if ignored:
- Minimum ID length: Both
user_idanddevice_idmust be at least 5 characters long. Shorter values are silently stripped from the event. If an event has neither a validuser_idnordevice_id, Amplitude rejects it. Reserved strings like"null","undefined","anonymous", and"none"are also invalid. - Time must be epoch milliseconds: Sending ISO 8601 timestamps in the
timefield returns a 400 error. However, every other field that represents a date should be ISO 8601. Yes, this is inconsistent. - String values cap at 1,024 characters: This includes
user_id, event names, and property values. If you are passing long JSON blobs as event properties, they will be silently truncated. - The
$skip_user_properties_syncflag: If you are sending high-volume events and do not need user property updates on every single event, set this flag totrue. It avoids hitting the silent 1,800/hour user property limit and reduces Amplitude's processing overhead. - 413 Payload Too Large thresholds differ by API: HTTP V2 rejects payloads over 1 MB. The Batch API rejects payloads over 20 MB. If you are dynamically batching events, your batch size logic needs to know exactly which endpoint you are targeting.
The Real Cost of a Custom Amplitude Integration
When a sales prospect asks for an Amplitude integration—much like when evaluating an Intercom integration—engineering teams often look at the documentation and estimate the work at a few days. They build a simple HTTP client, hardcode the JSON mappings, and ship it. This point-to-point approach is a trap.
The initial build is never the expensive part. The true cost of a custom integration lies in the permanent maintenance burden. Here is what happens next:
- Week 2: You discover the 30 events/sec limit during load testing and realize you need to build the durable queuing system detailed above.
- Month 2: A customer on a high-traffic plan is silently losing user property updates because you hit the 1,800/hour limit without realizing it.
- Month 4: Amplitude introduces new required fields or changes their error response format for 400 errors, and your custom parser breaks.
- Month 6: Your EU customers need data sent to
api.eu.amplitude.comand you realize you hardcoded the US endpoint in your core application logic. - Month 8: Enterprise Customer A wants the event named
Account Upgraded, while Customer B requires it to be namedsubscription_tier_changed. Hardcoding these mappings becomes impossible, forcing you to build a custom mapping UI.
This is the integration treadmill. Each point-to-point connection looks trivial in isolation, but the cumulative maintenance cost compounds. Every hour your senior engineers spend debugging a dropped Amplitude event is an hour they are not building your core product.
How Truto Simplifies Amplitude Integrations for B2B SaaS
Truto is designed specifically to eliminate the maintenance burden of customer-facing integrations. By utilizing a unified API and zero-code architecture, your team can offer a native Amplitude integration without writing or maintaining a custom API client.
Instead of writing custom Python or Node.js code for each integration, Truto uses a declarative configuration system. Every API connector is defined as a set of JSON configurations that map your data model to the provider's API schema using JSONata expressions. This means you have zero integration-specific code in your runtime.
What this means for your Amplitude integration specifically:
Transparent Rate Limit Handling
When Amplitude returns a 429, Truto passes that response directly to your application with standardized ratelimit-limit, ratelimit-remaining, and ratelimit-reset headers following the IETF specification. Your code decides how to handle backoff cleanly—Truto does not silently retry or absorb errors on your behalf. This is a deliberate design choice: opaque retry middleware can drop events or create duplicates without your internal systems ever knowing.
Unified Webhook Fan-Out
If you need to ingest events from your SaaS application and reliably route them to Amplitude, you can leverage Truto's unified webhook infrastructure. Your application simply fires a standard webhook to Truto. Truto handles the ingestion, applies the per-customer JSONata transformations to match their specific Amplitude taxonomy, and manages the outbound delivery to the Amplitude Batch API without requiring you to build a custom listener or queue.
Schema Mapping Without Code
Adding Amplitude (or switching from Amplitude to Mixpanel, or adding a HubSpot integration) is a configuration change, not a code change. The JSONata mapping layer transforms your unified event schema to whatever the target API expects, handling the nuances of custom taxonomies instantly.
There is an honest trade-off here. Using a unified API platform means adding a dependency in your data path. For teams that need absolute control over every byte sent to Amplitude and have dedicated integration engineers, building in-house makes sense. But for teams that need to ship 10+ integrations this quarter without draining their core engineering resources, the abstraction layer pays for itself rapidly.
What to Do Next
If you are building an Amplitude integration today, start with these steps:
- Choose your endpoint: Real-time user actions go to HTTP V2. Backfills, syncs, and server-side pipelines go to Batch. Do not try to use one endpoint for both.
- Always send
insert_id: Event deduplication is not optional in production. Generate a UUID for every single event payload. - Decouple ingestion from your app: Use an internal message queue between your application and Amplitude. Partition by
user_idordevice_idto isolate throttling. - Monitor user property sync separately: The 1,800/hour limit is silent. Track how many property updates you are sending per user and alert when you approach the threshold.
- Evaluate build vs. buy honestly: If Amplitude is your only analytics integration and your event volume is low, building in-house is reasonable. If you need to support multiple analytics platforms across different customer segments, the maintenance cost of point-to-point integrations compounds fast, a challenge we've explored in how mid-market SaaS teams handle API rate limits and webhooks at scale.
The Amplitude API itself is well-designed and well-documented compared to many enterprise SaaS APIs. The pitfalls are in the operational details—rate limits, silent property drops, endpoint-specific payload limits—that only surface under heavy production load. Plan for them upfront, and you will save yourself a painful midnight debugging session later.
FAQ
- Should I use the Amplitude HTTP V2 API or the Batch Event Upload API?
- Use the HTTP V2 API for real-time, low-volume event tracking where sequence matters. For high-volume server-to-server integrations, historical data migrations, or webhook fan-outs, you must use the Batch API, which accepts up to 1000 events per second per device ID.
- What is the rate limit for the Amplitude HTTP V2 API?
- Amplitude enforces a strict limit of 30 events per second for individual users or devices on the HTTP V2 API, measured as an average over a 30-second window. Exceeding this triggers HTTP 429 errors.
- How do you prevent duplicate events in Amplitude?
- Include an insert_id field in every event. Amplitude deduplicates events with the same device_id and insert_id within a rolling 7-day window. Generate a UUID or combine device_id, user_id, event_type, and timestamp.
- What happens when Amplitude's user property update limit is exceeded?
- Amplitude silently drops user property updates after 1,800 updates per hour per user, but continues ingesting standard events normally. There is no error response, making this limit particularly dangerous to miss in production.
- How does Truto handle Amplitude 429 rate limit errors?
- Truto passes upstream 429 rate limit errors directly back to the caller without applying internal retries or opaque backoff. It normalizes the rate limit data into standard IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) so your system can dynamically handle retries.