Developer API
Carousel
Generation API
Send text and a theme ID. Get back CDN-hosted carousel image URLs. One endpoint, synchronous response, permanent image URLs.
Ready to subscribe? See API pricing plans.
1 — Overview
Base URL
https://api.bulkinsta.com/v1 Versioning
The API version is part of the URL path (/v1). Breaking changes increment the version to /v2. Old versions receive at minimum 6 months deprecation notice before sunset.
Non-breaking additions — new optional fields, new themes, new error codes — are made to the current version without notice.
Request format
All request bodies must be JSON with Content-Type: application/json. All responses are JSON.
2 — Authentication
Header
| Key format | bik_live_[32 alphanumeric chars] |
| Obtained from | Dashboard > API Keys |
| Expiry | Keys do not expire unless manually revoked |
| Invalid key | 401 invalid_api_key |
| Revoked key | 401 expired_api_key |
Security
Never expose your API key in client-side code. Make all requests server-side. If compromised, revoke immediately from the dashboard.
3 — Endpoint
Generates a fully designed Instagram carousel from plain text and a theme. Synchronous — the connection stays open while slides render and upload to CDN. Returns once all slides are available.
Sync / timeout contract
- — Synchronous. No polling or webhooks required.
- — Max server timeout: 90 seconds. Set your HTTP client timeout to at least 120 s.
- — If server timeout is exceeded: 500 generation_failed. No quota consumed.
- — Typical response time: 5–30 seconds depending on slide count and theme.
Idempotency & retries
- — Not idempotent. Each call creates a new job and consumes one quota unit.
- — Retrying the same payload produces a new id and new image URLs.
- — Safe to retry on 500 — no quota consumed on server failures.
- — Do not retry 400, 401, or 404 — client errors that won't self-resolve.
Request Body
| Field | Type | Required | Constraints |
|---|---|---|---|
| text | string | Yes | 1–5000 characters. Plain text. Unicode and emoji supported. |
| theme_id | number | Yes | Integer. Enum: 1–10, 20–26, 30. See themes section. |
| brand_name | string | No | 0–60 characters. Brand / account label shown on slides. Defaults to null. |
Example Request
curl -X POST https://bulkinsta.com/api/v1/generate-carousel \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "5 habits that changed my mornings forever...",
"theme_id": 1,
"brand_name": "Acme Co."
}' Response Fields (200 OK)
| Field | Type | Description |
|---|---|---|
| id | string | Unique job ID. Format: gen_ + 10 alphanumeric chars. |
| slides | string[] | Ordered array of CDN image URLs. Length matches slide_count. |
| slide_count | number | Integer, 1-20. Determined by content length and theme layout. |
| theme_id | number | Echoes the theme ID used for this generation. |
| brand_name | string | null | Brand name as provided, or null if omitted. |
| created_at | string | ISO 8601 UTC timestamp of job creation. |
Example Response (200 OK)
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1744280520
{
"id": "gen_4f8a2c1d9e",
"slides": [
"https://cdn.bulkinsta.com/api-images/gen_4f8a2c1d9e/slide-1.png",
"https://cdn.bulkinsta.com/api-images/gen_4f8a2c1d9e/slide-2.png",
"https://cdn.bulkinsta.com/api-images/gen_4f8a2c1d9e/slide-3.png",
"https://cdn.bulkinsta.com/api-images/gen_4f8a2c1d9e/slide-4.png"
],
"slide_count": 4,
"theme_id": 1,
"brand_name": "Acme Co.",
"created_at": "2026-04-10T09:41:00Z"
} 4 — TypeScript Types
Copy-paste ready types for TypeScript consumers. The ApiError shape is shared across all error responses — branch on error, not message.
// Request
interface GenerateCarouselRequest {
text: string; // 1-5000 characters, plain text
theme_id: number; // integer; see themes list
brand_name?: string; // optional, 0-60 characters
}
// Success response (200 OK)
interface GenerateCarouselResponse {
id: string; // "gen_" + 10 alphanumeric chars
slides: string[]; // ordered CDN URLs, one per slide
slide_count: number; // 1-20
theme_id: number;
brand_name: string | null;
created_at: string; // ISO 8601 UTC
}
// Error response (all 4xx / 5xx)
interface ApiError {
error: string; // machine-readable snake_case code
message: string; // human-readable — do not parse
status: number; // mirrors HTTP status
} 5 — Errors
All errors follow the same JSON shape. Branch on the error field for machine-readable handling. The message field is for humans only — do not parse it.
| Field | Type | Description |
|---|---|---|
| error | string | Machine-readable snake_case code. Stable across versions. |
| message | string | Human-readable description. May change without notice. |
| status | number | Mirrors the HTTP status code. |
| Status | error code | Retryable | Quota consumed |
|---|---|---|---|
| 400 | invalid_request | No | No |
| 401 | invalid_api_key | No | No |
| 401 | expired_api_key | No | No |
| 402 | quota_exceeded | After upgrade | No |
| 404 | theme_not_found | No | No |
| 429 | rate_limit_exceeded | Yes (Retry-After) | No |
| 500 | generation_failed | Yes | No |
Response Examples
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_request",
"message": "text is required and must be between 1 and 5000 characters",
"status": 400
} HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "invalid_api_key",
"message": "The API key provided is not valid",
"status": 401
} HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "expired_api_key",
"message": "The API key has been revoked. Generate a new key from your dashboard",
"status": 401
} HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": "quota_exceeded",
"message": "Monthly generation quota reached. Upgrade your plan to continue",
"status": 402
} HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "theme_not_found",
"message": "theme_id 99 does not exist. See available themes in the documentation",
"status": 404
} HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 17
{
"error": "rate_limit_exceeded",
"message": "Rate limit of 10 requests/minute exceeded. Retry after 17 seconds",
"status": 429
} HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "generation_failed",
"message": "Carousel rendering failed. No quota was consumed. Please retry",
"status": 500
} 6 — Rate Limits
| Plan | Limit | Window |
|---|---|---|
| Starter | 10 requests | per minute |
| Growth | 30 requests | per minute |
| Scale | 60 requests | per minute |
Every response includes rate limit headers. On 429, wait the number of seconds in Retry-After before retrying.
X-RateLimit-Limit: 10 # requests allowed per minute X-RateLimit-Remaining: 7 # requests left in current window X-RateLimit-Reset: 1744280520 # Unix timestamp when window resets Retry-After: 17 # seconds to wait (only present on 429)
7 — CDN Behavior
URL format
| Format | PNG |
| Dimensions | 1080 x 1440 px (4:5 portrait) |
| Access | Public, no authentication required |
| Expiry | URLs do not expire |
| Slide index | n starts at 1 (slide-1.png, slide-2.png, ...) |
| Availability | Guaranteed by response time — slides are uploaded before response is returned |
8 — Edge Cases
| Input condition | Behavior |
|---|---|
| text is empty string | 400 invalid_request — text must be at least 1 character |
| text exceeds 5000 characters | 400 invalid_request — text is too long |
| text contains only whitespace | 400 invalid_request — text cannot be blank |
| text with emoji / unicode | Supported. Rendered as-is in slides. |
| theme_id is a string "1" | 400 invalid_request — theme_id must be a number |
| theme_id is a float e.g. 1.5 | 400 invalid_request — theme_id must be an integer |
| theme_id valid but wrong type | Accepted — no type-vs-content validation |
| brand_name over 60 characters | 400 invalid_request — brand_name too long |
| brand_name is null explicitly | Treated as omitted — null in response |
| Very short text (1-10 words) | Generates 1-2 slides. No error. |
9 — Themes
Pass the numeric id as theme_id. The type field indicates intended content format — mixing types is accepted but may produce suboptimal layouts.
| ID | Name | Type |
|---|---|---|
| 1 | Energetic | text_carousel |
| 2 | Calm | text_carousel |
| 3 | Editorial | text_carousel |
| 4 | Bold | text_carousel |
| 5 | Fresh | text_carousel |
| 6 | Bold Fun | text_carousel |
| 7 | Retro | text_carousel |
| 8 | Groovy | text_carousel |
| 9 | Neo-Editorial | text_carousel |
| 10 | Minimal | text_carousel |
| 20 | Crimson | quote_carousel |
| 21 | Soft Sage | quote_carousel |
| 22 | Dusty Lavender | quote_carousel |
| 23 | Warm Sand | quote_carousel |
| 24 | Blush Minimal | quote_carousel |
| 25 | Cool Sky | quote_carousel |
| 26 | Onyx | quote_carousel |
| 30 | Bold Editorial | comparison |
10 — Changelog
2026-04-10
Initial release. POST /generate-carousel, 18 themes, brand_name field.
Ready to create?
Ready to integrate?
Get your API key from your dashboard and start generating in minutes.