Alurnya
- Simpan desain di editor, lalu salin ID-nya dari alamat editor (/editor/[id]).
- Buat token API di Pengaturan → Token API (khusus pemilik ruang kerja).
- Kirim data ke POST /api/render/batch.
- Tunggu webhook render.finished, atau cek status lewat GET /api/render/jobs/[id].
- Unduh ZIP berisi semua hasil, atau ambil tautan per penerima.
Autentikasi
Kirim token di header Authorization pada setiap permintaan. Token bisa dicabut kapan saja di Pengaturan. Jangan simpan token di aplikasi browser atau mobile.
Authorization: Bearer bd_…Membuat video atau gambar
Satu permintaan = satu job. Setiap baris di rows menjadi satu video (atau satu gambar untuk desain gambar). Baris yang datanya tidak lengkap atau tidak valid dilewati dan dilaporkan; baris lain tetap dibuat.
| Field | Keterangan |
|---|---|
projectId | ID desain yang tersimpan di ruang kerja ini. Versi terakhir yang disimpan yang dirender. Wajib, kecuali kamu mengirim project. |
project | Alternatif projectId: seluruh JSON desain (seperti yang dikirim editor). |
rows | Daftar baris data (1–1.000). Nilai boleh teks, angka, true/false, atau null (kosong). |
columnMapping | Opsional. Pasangan kolom data desain → nama field di rows. Jika tidak dikirim, setiap kolom data membaca field dengan nama yang sama. |
fileNameTemplate | Opsional. Pola nama file dari kolom data, mis. {{kota}}-{{nama_pelanggan}}. |
webhookUrl | Opsional. Alamat HTTPS yang dipanggil saat job selesai. |
Contoh
curl -X POST https://<server>/api/render/batch \
-H "Authorization: Bearer $BEYOND_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"projectId": "AbC123xyz",
"rows": [
{ "nama_pelanggan": "Budi", "diskon": "20%", "kode_promo": "BUDI20" },
{ "nama_pelanggan": "Sari", "diskon": 25, "kode_promo": "SARI25" }
],
"fileNameTemplate": "{{nama_pelanggan}}",
"webhookUrl": "https://crm.contoh.id/hooks/beyond"
}'Desain gambar
projectId boleh menunjuk desain gambar (dibuat lewat Buat → Gambar). Job-nya membuat satu PNG (atau JPEG, sesuai pengaturan desain) per baris dengan antrean, tautan, ZIP, dan webhook yang sama; job.medium bernilai "image" dan nama file berakhiran .png/.jpg. Gambar dan video dihargai terpisah (GET /api/credits/pricing). Mengirim project langsung: sertakan "medium": "image" dan tepat satu scene tanpa audio.
Respons
202 berarti job sudah masuk antrean. Simpan job.id untuk mengecek status.
HTTP/1.1 202 Accepted
{
"job": {
"id": "Xq3vT9aLm2Pk",
"kind": "batch",
"medium": "video",
"state": "queued",
"total": 2,
"done": 0,
"failed": 0,
"skipped": 0,
"progress": 0,
"errors": [],
"error": null,
"createdAt": "2026-10-01T08:00:00.000Z",
"finishedAt": null,
"downloadable": false,
"deleteAfter": "2026-10-31T08:00:00.000Z",
"voice": { "pending": 2, "total": 2 }
},
"credits": { "charged": 8, "videos": 4, "voice": 4, "balance": 42 }
}Kode error umum:
400Isi permintaan tidak valid (detail di issues).401Token tidak ada atau sudah dicabut.402Kredit tidak cukup (params: saldo dan kebutuhan). Harga video (menurut durasi), gambar, dan halaman ada di GET /api/credits/pricing, suara AI dinamis dihitung per karakter; tukar kode promo di halaman Kredit.403Desain memakai template, elemen, atau suara AI Pro, sementara paket ruang kerja Free.404Desain tidak ditemukan di ruang kerja ini.413Terlalu banyak baris atau video terlalu panjang.422Tidak ada baris yang lengkap, atau ada media yang tidak bisa diambil.429Terlalu banyak permintaan. Coba lagi setelah Retry-After detik.
Suara AI dinamis
Desain dengan suara AI yang berisi kolom data (mis. Halo {{nama_pelanggan}}) membuat satu suara per baris sebelum videonya dirender (job.voice menunjukkan baris yang masih menunggu suaranya). Biayanya per karakter setelah kolom diisi — spasi dan tanda baca dihitung, spasi/baris baru berurutan dihitung satu, tag <…> tidak — dibulatkan ke atas per suara, sesuai harga yang diatur admin. Estimasi menulis jumlahnya di voice.characters; voice.words masih dikirim tapi usang dan akan dihapus; suara yang sama persis tidak ditagih dua kali. Kredit suara dipotong bersama kredit video saat job dibuat (credits.voice) dan dikembalikan untuk baris yang suaranya gagal dibuat.
Untuk mengetahui biaya sebelum membuat job, kirim isi yang sama ke POST /api/render/estimate. Tidak ada yang masuk antrean dan tidak ada kredit yang dipotong.
POST /api/render/estimate (same body as /api/render/batch)
HTTP/1.1 200 OK
{
"estimate": {
"medium": "video",
"items": 2,
"creditsPerItem": 2,
"durationSeconds": 12.5,
"credits": 4,
"videos": 2,
"voice": { "clips": 2, "characters": 48, "words": 8, "credits": 4 },
"total": 8,
"balance": 50
}
}Harga kredit
Harga diatur admin dan bisa berubah; ambil yang berlaku sekarang dari GET /api/credits/pricing (sesi atau token API). Harga video mengikuti durasi video jadi: rentang dengan batas atas upToSeconds (inklusif, naik), rentang terakhir (null) untuk semua video yang lebih panjang. Job menyimpan harga saat dibuat, jadi perubahan harga tidak memengaruhi job yang sudah berjalan atau pengembaliannya. Estimasi membawa creditsPerItem dan durationSeconds (null untuk gambar). Tiap suara AI punya harga per karakter sendiri; voice.fromCreditsPerChar adalah yang termurah (null bila suara AI mati). Kredit dibeli (top up) dan langganan Pro dibayar pemilik ruang kerja di halaman Kredit (invoice, QRIS/e-wallet/kartu/gerai retail); tidak ada API publik untuk pembayaran.
GET /api/credits/pricing
HTTP/1.1 200 OK
{
"pricing": {
"video": [
{ "upToSeconds": 10, "credits": 1 },
{ "upToSeconds": 30, "credits": 2 },
{ "upToSeconds": null, "credits": 5 }
],
"image": 1,
"landingPage": 1
},
"voice": { "fromCreditsPerChar": 0.08 }
}Status dan hasil
state berubah dari queued ke rendering lalu done (atau failed/cancelled). progress bernilai 0–1. Hasil tersedia setelah job selesai dan dihapus otomatis sesuai masa simpan.
GET /api/render/jobs/{id}Status job dan masalah per baris.GET /api/render/jobs/{id}/downloadZIP berisi semua MP4 atau gambar, _laporan.txt, _tautan.csv, dan _tautan.xlsx.GET /api/render/jobs/{id}/linksTautan tonton per penerima (halaman tonton menampilkan gambar untuk job gambar). Sebagai file dengan ?format=csv atau ?format=xlsx.
Campaign
Campaign menyimpan satu desain, daftar penerima, dan hasil render per baris. Buat campaign di menu Campaign (atau POST /api/campaigns), lalu dari CRM kamu tambahkan penerima dan mulai render. Kunci tiap baris adalah nama kolom data desain dan kolom tambahan campaign (bukan judul kolom CSV). Hanya satu render per campaign yang bisa berjalan; Mulai saat render lain berjalan dijawab 409.
GET /api/campaignsDaftar campaign (?projectId= untuk satu desain).POST /api/campaignsBuat campaign: name, description, kind (video | landing | landing_video | image | landing_image), projectId (desain video untuk video/landing_video, desain gambar untuk image/landing_image), landingPageId (landing page), slug (wajib untuk jenis landing; unik global), fields, fileNameTemplate.PUT /api/campaigns/{id}/rowsTambah (mode append) atau ganti (mode replace) penerima, maksimal 500 baris per permintaan.GET /api/campaigns/{id}/rowsSemua baris dengan status (pending, invalid, queued, voice, rendering, done, failed, skipped, cancelled, live), keterangan, tautan tonton, slug, dan pageUrl (halaman penerima).POST /api/campaigns/{id}/estimatePerkiraan kredit untuk Mulai, tanpa memotong kredit.POST /api/campaigns/{id}/startMulai render; kredit dipotong saat itu (402 creditInsufficient jika kurang).POST /api/campaigns/{id}/cancelBatalkan render yang berjalan; kredit untuk hasil yang belum jadi dikembalikan.GET|POST /api/campaigns/{id}/publishJenis landing: GET = perkiraan (halaman, halaman baru, kredit); POST = terbitkan semua baris valid dengan landing page versi sekarang. Landing: harga per halaman baru dari GET /api/credits/pricing (402 jika kurang); landing_video dan landing_image: gratis. Opsional webhookUrl → event campaign.published.POST /api/campaigns/{id}/unpublishHentikan semua halaman campaign (404) tanpa menghapus data; terbitkan ulang tidak menagih lagi.GET /api/campaigns/slug-availableCek apakah slug campaign bisa dipakai (?slug=, opsional &campaign= untuk campaign yang sedang diubah).GET /api/campaigns/{id}/exportCSV berisi semua kolom, status, keterangan, file, tautan tonton, dan jumlah tayang; Excel dengan ?format=xlsx.GET /api/campaigns/{id}/analyticsAnalitik (Phase 40): penerima, siap, ditonton/dilihat/diklik (unik = baris, total = kejadian), email terkirim/diklik, WhatsApp terkirim/diterima/dibaca/gagal per broadcast dan channel, dan angka per hari dalam zona waktu ?tz= (IANA, bawaan Asia/Jakarta). Kunjungan tim sendiri dan pratinjau tautan tidak dihitung.GET /api/campaigns/{id}/analytics/rowsKeterlibatan per baris (?cursor=, ?limit= ≤ 1000): jumlah tonton, waktu pertama/terakhir, halaman, status email dan WhatsApp terakhir — tanpa nilai kolom data. nextCursor = halaman berikutnya.GET /api/campaigns/{id}/analytics/exportCSV analitik per baris; waktu dalam ?tz=, kolom data hanya dengan ?data=1; Excel dengan ?format=xlsx.
# 1. Add recipients (keys = data fields of the design + extra fields; ≤ 500 rows per request)
curl -X PUT https://<server>/api/campaigns/Cmp42aBcD9xy/rows \
-H "Authorization: Bearer $BEYOND_TOKEN" -H "Content-Type: application/json" \
-d '{ "mode": "append", "rows": [ { "nama_pelanggan": "Budi", "no_hp": "0812 3456 7890" } ] }'
# 2. Render every row without a video yet ("scope": "pending" | "all" | "rows" + "rows": [0, 4])
curl -X POST https://<server>/api/campaigns/Cmp42aBcD9xy/start \
-H "Authorization: Bearer $BEYOND_TOKEN" -H "Content-Type: application/json" \
-d '{ "scope": "pending", "webhookUrl": "https://crm.contoh.id/hooks/beyond" }'
HTTP/1.1 202 Accepted
{ "campaign": { "id": "Cmp42aBcD9xy", "status": "running", … }, "job": { "id": "Xq3vT9aLm2Pk", … },
"credits": { "charged": 2, "videos": 2, "voice": 0, "balance": 48 } }Berbeda dengan /api/render/batch, data penerima campaign disimpan di server selama campaign ada (atau sampai masa simpan server habis) dan ikut terhapus saat campaign dihapus. Webhook dari render campaign menyertakan campaignId.
Campaign jenis landing, landing_video, dan landing_image membuat satu halaman per baris di /campaign/<slug campaign>/<slug baris>. Kolom slug wajib di tiap baris: nilainya dinormalkan (Budi Santoso → budi-santoso) dan slug yang sama diberi kode acak. Halaman hanya menampilkan kolom data landing page (kolom tambahan tidak pernah tampil), tidak diindeks mesin pencari, dan tautan tombol dicatat lewat pengalihan untuk statistik klik. Di landing_image, gambar personal baris juga menjadi gambar pratinjau tautan (og:image).
# 1. A landing page campaign (the slug is the first part of every recipient's URL)
curl -X POST https://<server>/api/campaigns \
-H "Authorization: Bearer $BEYOND_TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "Promo Oktober", "kind": "landing", "landingPageId": "Lp7sK2mQ9xYz", "slug": "promo-oktober" }'
# 2. Recipients need a slug (their page address)
curl -X PUT https://<server>/api/campaigns/Cmp42aBcD9xy/rows \
-H "Authorization: Bearer $BEYOND_TOKEN" -H "Content-Type: application/json" \
-d '{ "mode": "append", "rows": [ { "slug": "budi-santoso", "nama_pelanggan": "Budi" } ] }'
# 3. Publish: https://<server>/campaign/promo-oktober/budi-santoso
curl -X POST https://<server>/api/campaigns/Cmp42aBcD9xy/publish \
-H "Authorization: Bearer $BEYOND_TOKEN" -H "Content-Type: application/json" -d '{}'
HTTP/1.1 200 OK
{ "campaign": { "id": "Cmp42aBcD9xy", "status": "live", … }, "credits": { "charged": 1, "balance": 48 } }Webhook
Saat job selesai, kami mengirim POST JSON ke webhookUrl. Setiap kiriman ditandatangani dengan rahasia webhook ruang kerja kamu (Pengaturan → Token API → Rahasia webhook).
POST <webhookUrl>
Content-Type: application/json
X-Beyond-Event: render.finished
X-Beyond-Timestamp: 1790000000
X-Beyond-Signature: v1=5d41402abc4b2a76b9719d911017c592…
{
"event": "render.finished",
"job": { "id": "Xq3vT9aLm2Pk", "state": "done", "total": 2, "done": 2, … },
"campaignId": "Cmp42aBcD9xy",
"downloadPath": "/api/render/jobs/Xq3vT9aLm2Pk/download",
"linksPath": "/api/render/jobs/Xq3vT9aLm2Pk/links"
}Webhook campaign.published (dari POST …/publish dengan webhookUrl) memakai tanda tangan yang sama, berisi campaignId, publishedAt, dan pages: [{ row, slug, pagePath }]. Webhook render.finished dari campaign landing_video menyertakan pages: [{ row, pagePath }].
Cara memeriksa di sistem penerima: hitung HMAC-SHA256 dari timestamp, titik, lalu body mentah, dengan rahasia webhook sebagai kunci. Bandingkan hasilnya (hex) dengan nilai setelah v1=, dan tolak kiriman yang timestamp-nya berselisih lebih dari 5 menit dari jam server kamu.
// Node.js (Express): use the raw body, not re-serialised JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
app.post("/hooks/beyond", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Beyond-Timestamp");
const signature = req.get("X-Beyond-Signature") ?? "";
const body = req.body.toString("utf8");
const expected = "v1=" + createHmac("sha256", process.env.BEYOND_WEBHOOK_SECRET)
.update(`${timestamp}.${body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const valid = fresh && expected.length === signature.length
&& timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!valid) return res.sendStatus(401);
const { job } = JSON.parse(body);
// … update the CRM with job.id / job.state
res.sendStatus(204);
});Webhook dikirim sekali (batas waktu 10 detik). Jika sistem kamu sedang mati, cek status job lewat API.
Batasan
- Setiap video dan gambar memakai kredit sesuai harga saat ini (video menurut durasinya; lihat GET /api/credits/pricing), dipotong saat job masuk antrean (respons berisi credits.charged dan credits.balance). Yang gagal atau dibatalkan dikembalikan otomatis.
- 20 permintaan render per menit.
- Maksimal 1.000 baris per job dan 5 menit per video (bisa berbeda di server kamu).
- Media (gambar, video) di data harus bisa diakses publik lewat HTTPS; alamat jaringan internal ditolak.
- Data baris hanya dipakai saat render dan dihapus setelah video selesai.
- Suara AI dinamis memakai kredit per karakter, termasuk spasi (dibulatkan ke atas per suara), di luar kredit video; cek dulu dengan POST /api/render/estimate.
