Testivora API
Read and manage your testimonials, spaces, and walls programmatically. For agencies and clients integrating Testivora into their own flows and tools.
Authentication
Every request carries a Bearer API key, created in Settings → API. The key is account-scoped: it only sees your account's data. We store only the key's hash — you'll see it in full just once.
curl https://api.testivora.com/v1/account \
-H "Authorization: Bearer tv_live_xxxxxxxxxxxxxxxxxxxx"Keys have scopes: read (GET) and/or write (POST/PATCH). A read-only key can't create or modify.
Errors
Every error uses the same { error: { type, message } } envelope with the right HTTP status:
401— missing header, or invalid/revoked key402 quota_exceeded— monthly plan quota reached (includes limit, used, tier)403— your plan has no API access, or the key lacks write scope404— resource not in your account422— validation error (empty text, rating outside 1-5, etc.)429 rate_limited— too many requests (honor the Retry-After header)
Rate limits
Per API key, independent of your monthly quotas:
- Reads: 120/min
- Writes: 30/min
These differ from the tier's monthly quotas (how many testimonials you can create per month).
Pagination
Lists are cursor-paginated: the response carries { object:"list", data, has_more, next_cursor }. Pass ?cursor=<next_cursor>&limit=50 for the next page. limit defaults to 25, max 100.
Media URLs
Video, thumbnail, and photo URLs are signed and expire (~1 hour). Re-fetch the resource for fresh URLs; don't store them long-term. External URLs you provided are returned unchanged.
Account
Your account: tier, API access, and usage vs. plan limits.
/v1/accountReturns the account object with current-period usage and limits (null = unlimited).
curl https://api.testivora.com/v1/account \
-H "Authorization: Bearer tv_live_xxxxxxxxxxxxxxxxxxxx"{
"object": "account",
"tier": "growth",
"api_access": true,
"period": "2026-06",
"usage": { "total_testimonials": 240, "testimonials_this_period": 12 },
"limits": { "testimonials_per_month": 500, "ai_credits_per_month": 3000 }
}Spaces
Your spaces (each space is a collection of testimonials for one product or service).
/v1/spacesLists all your spaces, most recent first.
curl https://api.testivora.com/v1/spaces \
-H "Authorization: Bearer tv_live_..."{
"object": "list",
"data": [
{
"id": "k57...",
"object": "space",
"name": "Mi curso",
"slug": "mi-curso",
"primary_color": "#a8412c",
"logo_url": "https://...signed...",
"testimonial_count": 240,
"approved_count": 180,
"public_wall_url": "https://testivora.com/wall/mi-curso"
}
],
"has_more": false,
"next_cursor": null
}/v1/spaces/{id}Returns one space by id.
Testimonials
The core of the API: read, import, and manage testimonials.
/v1/testimonialsLists testimonials within a space (cursor-paginated). Filters:
space_id— required. the space to list from.status— pending | approved | archived. Omit to return all.limit— default 25, max 100.cursor— the next_cursor from the previous page.
curl "https://api.testivora.com/v1/testimonials?space_id=SPACE_ID&status=approved&limit=50" \
-H "Authorization: Bearer tv_live_..."{
"id": "j97...",
"object": "testimonial",
"space_id": "k57...",
"status": "approved",
"type": "text",
"text": "Cerré 3 clientes en una semana. Brutal.",
"rating": 5,
"featured": true,
"tags": ["caso-b2b"],
"author": { "name": "Ana López", "title": "Coach", "company": "Acme", "photo_url": null },
"video": null,
"images": [],
"created_at": "2026-06-03T01:00:00.000Z",
"published_at": "2026-06-03T01:00:00.000Z"
}/v1/testimonials/{id}Returns one testimonial by id.
/v1/testimonialsCreates a TEXT testimonial (imports one collected elsewhere). Counts against your monthly quota. Only space_id and text are required.
curl -X POST https://api.testivora.com/v1/testimonials \
-H "Authorization: Bearer tv_live_..." \
-H "Content-Type: application/json" \
-d '{
"space_id": "SPACE_ID",
"text": "Cerré 3 clientes en una semana. Brutal.",
"rating": 5,
"status": "approved",
"tags": ["caso-b2b"],
"author": { "name": "Ana López", "title": "Coach", "company": "Acme" }
}'/v1/testimonials/{id}Update status (pending/approved/archived), the featured flag, and/or tags.
curl -X PATCH https://api.testivora.com/v1/testimonials/TESTIMONIAL_ID \
-H "Authorization: Bearer tv_live_..." \
-H "Content-Type: application/json" \
-d '{ "status": "approved", "featured": true }'Walls
Your walls. A full wall returns its config (template, theme, copy, blocks) PLUS its approved testimonials already filtered and ordered exactly like the public wall — ready to render anywhere.
/v1/wallsLists summaries of all your walls.
/v1/walls/{id} · /v1/walls?space_slug=Returns a full wall by id, or a space's published wall via ?space_slug=.
curl "https://api.testivora.com/v1/walls?space_slug=mi-curso" \
-H "Authorization: Bearer tv_live_..."Webhooks
Register a URL in Settings → Webhooks and we'll send a signed POST whenever something happens. Perfect for tagging in your CRM, pinging Slack, or triggering a Zap/n8n.
testimonial.created— a new testimonial was received (any status).testimonial.approved— a testimonial moved to approved.testimonial.deleted— a testimonial was deleted.
Every delivery carries this envelope (the id is stable across retries — use it to dedupe):
{
"id": "evt_3hF2bQ...",
"object": "event",
"event": "testimonial.approved",
"version": "1.0",
"created": "2026-06-03T01:00:00.000Z",
"attempt": 1,
"data": { /* the testimonial object (same shape as the API) */ }
}Verify it came from us: recompute the HMAC-SHA256 of the Testivora-Signature header with your secret and compare in constant time.
import crypto from "node:crypto";
// header = req.headers["testivora-signature"] -> "t=1717,v1=abc123…"
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected),
);
}Create your first API key from your dashboard and start pulling your testimonials. Available on Growth and Agency.
Create my API key →