API
Integrations
API
This documentation covers the RESTful API endpoints for managing customers in the system. All endpoints are protected by ApiClientMiddleware and require proper authentication.
Create a secret key
To get started, create a secret key to authenticate your API requests.
Use https://api.truebeep.com/v1 as the base URL for all TrueBeep API requests.
Create Customer
Creates a new customer record.
{
"success": true,
"data": {
"id": "e8c43l37t1zayx8t66t5xbg9",
"firstName": "John",
"lastName": "Doe",
"phone": "+177566302",
"points": 15
}
}
| Field | Type | Description |
|---|---|---|
firstName(Required) | string | This field is required |
lastName(Required) | string | This field is required |
phone(Conditional) | string | Min 10 digits |
email(Conditional) | string | Valid email |
username(Optional) | string | Optional |
Conditional fields indicate that at least one of the marked fields is required. For this endpoint, you must provide either phone or email (or both) along with the required fields.
Bulk Create Customers
Create multiple customer records in a single request.
[
{
"id": "uuid",
"firstName": "John",
"lastName": "Doe",
"phone": "1234567890",
"email": "john.doe@example.com"
},
{
"id": "uuid2",
"firstName": "Sam",
"lastName": "Smith",
"phone": "9876543210",
"email": "sam.smith@example.com"
}
]
| Field | Type | Description |
|---|---|---|
firstName(Required) | string | This field is required |
lastName(Required) | string | This field is required |
phone(Conditional) | string | Min 10 digits |
email(Conditional) | string | Valid email |
username(Optional) | string | Optional |
Conditional fields indicate that at least one of the marked fields is required. For this endpoint, you must provide either phone or email (or both) along with the required fields.
Update Customer
Update customer information.
{
"id": "uuid",
"firstName": "Walter",
"lastName": "White",
"phone": "+1234567890"
}
Get Customer
Retrieve customer information by ID.
{
"id": "uuid",
"firstName": "John",
"lastName": "Doe",
"phone": "1234567890",
"email": "john.doe@example.com",
"points": "100"
}
Update Customer QR Points
Update customer points using a QR code scan.
{
"success": true,
"data": {
"id": "jv3cstlguhr2e91oihi0bfsx",
"firstName": "John",
"lastName": "Doe",
"points": 2
}
}
| Field | Type | Description |
|---|---|---|
code(Required) | string | This field is required |
couponId(Required) | string | This field is required |
Update Customer Loyalty Points
Update a customer's loyalty points.
{
"id": "l7817f8lbdqn18pok75x2mnm",
"firstName": "John",
"lastName": "Doe",
"email": "john@gmail.com",
"points": 10
}
| Field | Type | Description |
|---|---|---|
points(Required) | number | This field is required |
type(Required) | increment or decrement | This field is required |
Forms API
The Forms API lets your own website, app, or backend submit responses into a TrueBeep form. You keep your existing front-end — the markup, styling, and validation stay yours — and TrueBeep becomes the place the submissions are stored, searched, exported, and turned into customer records.
Use it to:
- Replace a mailto or a bespoke form handler with a real inbox you can filter and export
- Consolidate many website forms into one TrueBeep form, tagging each submission with the page it came from
- Capture leads as customers automatically — a submission with an email or phone is matched to an existing customer or creates a new one
All Forms endpoints are protected by ApiClientMiddleware and use the same secret key as the rest of the API. Base URL https://api.truebeep.com/v1, with your key as a bearer token:
Authorization: Bearer <token>
Requests are scoped to the team that owns the key — you can only read and write your own team's forms.
Server-to-server only. Your secret key grants full write access to your team's data, so it must never be embedded in browser JavaScript or a mobile app binary. Post the form to your own backend first, then have your backend call this API.
How it works
Create the form
Build it in the Form Builder, or create it over the API with POST /forms. Either way the form defines the schema that submissions are validated against.
Read the field keys
Call the schema endpoint to get each field's key. Keys are what you submit against.
Submit responses
POST an array of key / value pairs. The response appears in your dashboard under Forms → View Responses immediately.
See the Forms guide for building and publishing a form.
About field keys
Every field has a key — a lowercase, underscore-separated identifier derived from its label. First Name becomes first_name, Email Address becomes email_address.
Keys are regenerated from the label every time the form is saved in the builder. Renaming a field label will change its key and silently break an integration that hard-codes the old one. Two further quirks to be aware of when naming fields:
- A question mark becomes a trailing
q—How did you find us?yieldshow_did_you_find_usq - Digits are replaced with letters derived from the field's internal id, so
Line 2yields an unpredictable key
Prefer labels without digits or question marks, and always read keys from the schema endpoint rather than guessing them.
Create a Form
Creates a form and all its fields in one request. Returns the generated formId and each field's server-generated key.
{
"success": true,
"data": {
"id": "q06e8ni8x0oksl2l23xxknjz",
"name": "Contact Us",
"status": "published",
"fields": [
{ "key": "first_name", "label": "First Name", "type": "varchar", "fieldType": "TextInput", "isRequired": true, "options": [] },
{ "key": "email_address", "label": "Email Address", "type": "varchar", "fieldType": "EmailInput", "isRequired": true, "options": [] },
{ "key": "region", "label": "Region", "type": "varchar", "fieldType": "Select", "isRequired": false, "options": ["Guam", "CNMI"] }
]
}
}
| Field | Type | Description |
|---|---|---|
name(Required) | string | Must be unique within your team |
fields(Required) | array | At least one field — see the field object below |
description(Optional) | string | Shown under the form title |
formPurpose(Optional) | string | Context used by AI form filling |
status(Optional) | string | draft or published. Default draft |
isPublic(Optional) | boolean | Default true |
createCustomerOnSubmit(Optional) | boolean | Default true |
webhookUrl(Optional) | string | POSTed the full response after each submission |
webhookSecret(Optional) | string | Sent as Authorization: Bearer on the webhook |
| Field | Type | Description |
|---|---|---|
fieldType(Required) | string | TextInput, Textarea, EmailInput, PhoneInput, NumberInput, DatePicker, Select, Radio, Checkbox |
label(Required) | string | Also the source of the generated key |
isRequired(Optional) | boolean | Default false |
options(Optional) | string[] | Required for Select, Radio, and checkbox groups |
isGroup(Optional) | boolean | Checkbox only — renders a multi-select group |
placeholder(Optional) | string | |
helperText(Optional) | string | Hint shown under the field |
defaultValue(Optional) | string | |
aiInstruction(Optional) | string | Guidance for AI-assisted form filling |
You cannot choose the field keys. They are generated from each label using the same rules the Form Builder uses, so that opening the form in the builder and saving it does not rename them. Read the keys from the create response, or from GET /forms/:formId/schema, before you write any code that submits to the form.
Field ordering follows the order of the fields array. type (the storage type) is derived from fieldType — you do not send it.
Update a Form
Updates a form's settings and, optionally, its fields. Every property is optional — send only what you want to change.
{
"success": true,
"data": {
"id": "q06e8ni8x0oksl2l23xxknjz",
"name": "Contact Us",
"status": "published",
"fields": [
{ "key": "first_name", "label": "First Name", "type": "varchar", "fieldType": "TextInput", "isRequired": true, "options": [] }
],
"removedFields": []
}
}
Accepts every body parameter from Create (except that name is optional), plus:
| Field | Type | Description |
|---|---|---|
fields(Optional) | array | Omit to leave fields untouched. If sent, this is the complete list the form ends up with |
removeMissingFields(Optional) | boolean | Confirms deletion of fields absent from fields. Default false |
How fields are matched
Fields are matched to the existing ones by label. A field whose label is unchanged keeps its id and key, so every answer already collected against it is preserved while its type, options, or required flag change freely. A field with a new label is created; renaming a label therefore creates a new field rather than renaming the old one.
Removing a field permanently deletes every answer already collected for it, across all past responses — it cannot be undone.
Because fields is a complete replacement list, simply forgetting to include an existing field would silently destroy data. The API therefore refuses the update, listing the fields at risk.
Re-send the missing fields to keep them, or set removeMissingFields: true to confirm you intend to delete them and their data.
{
"success": false,
"code": "FIELDS_WOULD_BE_REMOVED",
"message": "These fields are missing from the update and removing them deletes every answer already collected for them. Re-send them, or set removeMissingFields to true to confirm.",
"fields": ["how_did_you_find_us"]
}
Header, footer, and styling configured in the Form Builder are preserved on update — only the field list is rewritten.
List Forms
Returns every form belonging to your team, so you can find the formId you need without opening the dashboard.
{
"success": true,
"data": [
{
"id": "qo0zhyldtdiv7vxgtg6emq5r",
"name": "Contact Us",
"status": "published",
"provider": "internal",
"isPublic": true,
"responseCount": 128,
"createdAt": "2026-08-20T01:37:41.438Z",
"updatedAt": "2026-08-20T01:37:41.438Z"
}
]
}
Get Form Schema
Returns the form's fields, in the order they appear in the builder. Call this once when building your integration — and again whenever someone edits the form — to pick up the current keys and select options.
{
"success": true,
"data": {
"id": "qo0zhyldtdiv7vxgtg6emq5r",
"name": "Contact Us",
"status": "published",
"fields": [
{
"key": "first_name",
"label": "First Name",
"type": "varchar",
"fieldType": "TextInput",
"isRequired": true,
"options": [],
"maxLength": 255
},
{
"key": "who_is_this_directed_to",
"label": "Who is this directed to",
"type": "varchar",
"fieldType": "Select",
"isRequired": false,
"options": ["Customer Service / Tech Support", "Business Sales", "Ad Sales"],
"maxLength": 255
},
{
"key": "comment",
"label": "Comment",
"type": "text",
"fieldType": "Textarea",
"isRequired": false,
"options": [],
"maxLength": null
}
]
}
}
| Field | Type | Description |
|---|---|---|
key | string | The identifier you submit against |
label | string | Human-readable label as shown on the form |
type | string | Storage type — varchar, text, number, boolean, or date |
fieldType | string | Builder component — TextInput, Select, Checkbox, Textarea, ... |
isRequired | boolean | Submissions missing this field are rejected |
options | string[] | Allowed values for Select, Radio, and checkbox groups. Empty otherwise |
maxLength | number | 255 for varchar fields, null when unlimited |
Submit a Form Response
Creates a form response. Returns the new response id and, when a customer could be matched or created, the customer id.
{
"success": true,
"data": {
"formResponseId": "e8c43l37t1zayx8t66t5xbg9",
"subscriberId": "jv3cstlguhr2e91oihi0bfsx"
}
}
| Field | Type | Description |
|---|---|---|
responses(Required) | array | At least one { key, value } pair |
responses[].key(Required) | string | A key from the schema endpoint |
responses[].value(Required) | mixed | String, number, boolean, or array of strings — see the table below |
customer(Optional) | object | Overrides the customer details derived from the submission |
customer.firstName(Optional) | string | Optional |
customer.lastName(Optional) | string | Optional |
customer.email(Optional) | string | Valid email |
customer.phone(Optional) | string | Optional |
Value formats by field type
The type returned by the schema endpoint determines what a value may be:
type | Accepted value | Notes |
|---|---|---|
varchar | string, number, or string[] | Max 255 characters. For Select / Radio the value must be one of options. Checkbox groups take an array and are stored pipe-joined |
text | string | No length limit — use this for long free-text and URLs |
number | number or a numeric string | Must be finite |
boolean | true / false, or "true" / "false" | |
date | ISO 8601 string | e.g. 2026-08-20T00:00:00.000Z |
varchar fields are capped at 255 characters and a longer value is rejected with INVALID_FIELD_VALUES. If you need to send something long — a page URL with tracking parameters, a plan summary, a free-text comment — make that field a Textarea in the builder so it is stored as text instead.
Validation rules
- Required fields are enforced. Any field with
isRequired: truethat is missing,null, or blank rejects the whole submission with400 MISSING_REQUIRED_FIELDS. - Unknown keys are ignored, not rejected. A key that does not exist on the form is dropped and echoed back in
data.ignoredKeysso typos surface without failing live traffic. Always check this array while building your integration. - Blank optional fields are skipped. They are not stored as empty values.
- Select and Radio values are checked against
options, so a value that does not match is rejected rather than silently stored.
{
"success": true,
"data": {
"formResponseId": "e8c43l37t1zayx8t66t5xbg9",
"subscriberId": null,
"ignoredKeys": ["emial_address"]
}
}
List Form Responses
Returns a form's responses, newest first, with pagination and filtering.
{
"success": true,
"data": [
{
"id": "kt36fnwwr0ni3dk6biksw8nf",
"createdAt": "2026-08-20T02:40:06.967Z",
"source": "public",
"isRead": false,
"formId": "qo0zhyldtdiv7vxgtg6emq5r",
"formName": "Contact Us",
"subscriberId": "v863ihow50ztrh8kserqhvjx",
"subscriberFirstName": "John",
"subscriberLastName": "Doe",
"fields": [
{ "key": "first_name", "label": "First Name", "type": "varchar", "fieldType": "TextInput", "value": "John" },
{ "key": "inquiry_source", "label": "Inquiry Source", "type": "varchar", "fieldType": "Select", "value": "fiber" },
{ "key": "comment", "label": "Comment", "type": "text", "fieldType": "Textarea", "value": "Please call me." }
]
}
],
"pagination": { "page": 1, "limit": 20, "total": 137, "totalPages": 7 }
}
| Field | Type | Description |
|---|---|---|
page(Optional) | number | Default 1 |
limit(Optional) | number | Default 20, max 100 |
order(Optional) | string | asc or desc by submission time. Default desc |
source(Optional) | string | public (form or API) or ai (collected by AI auto-reply) |
isRead(Optional) | string | true or false |
startDate(Optional) | string | ISO date; filters createdAt >= |
endDate(Optional) | string | ISO date; filters createdAt <= |
search(Optional) | string | Case-insensitive match across every answer on the response |
fieldKey(Optional) | string | Filter on one field, e.g. inquiry_source. Requires fieldValue |
fieldValue(Optional) | string | Exact value the field must equal. Requires fieldKey |
fieldKey + fieldValue is what makes one shared form workable across many pages. If every page sets its own inquiry_source, then ?fieldKey=inquiry_source&fieldValue=fiber gives you just that page's submissions — no separate form needed per page.
fields always lists every field currently on the form, in builder order. A field the responder left blank — or one added to the form after the response was collected — comes back with value: null, so the shape is stable across rows and easy to turn into table columns or CSV.
Get a Form Response
Returns a single response in the same shape as a list entry. Responds 404 RESPONSE_NOT_FOUND if the response does not exist, is not on that form, or belongs to another team.
Customer matching
When a submission contains an email or phone number, TrueBeep looks for an existing customer on your team with that channel and links the response to them, creating a new customer if there is no match. The returned subscriberId is that customer.
Values are taken from the submission automatically:
- Email — the first field with
fieldType: "EmailInput" - Phone — the first field with
fieldType: "PhoneInput" - Name — the
first_nameandlast_namefields, or a singlenamefield split on the first space
Send a customer object to override any of these. If a submission has neither an email nor a phone, subscriberId is null and the response is still stored.
Customer matching never fails a submission. If the customer record cannot be created, the form response is still saved and subscriberId comes back null.
Turning customer creation off
Customer creation is controlled by the form, not by the request. Open the form in the Form Builder → Settings → Customer and switch Create Customer On Submit off to collect responses without adding anyone to your customer list.
It is on by default, and the setting applies to every route into the form — this API and the public form page alike — so all your pages behave consistently and no integration can opt itself out.
The current value is returned by the schema endpoint:
{
"success": true,
"data": {
"id": "qo0zhyldtdiv7vxgtg6emq5r",
"name": "Contact Us",
"createCustomerOnSubmit": true,
"fields": []
}
}
With it off, submissions still succeed and every answer is still stored — including name, email and phone, which remain visible in the responses table and in exports. You simply get subscriberId: null and nobody is added to your customer list.
Switching it off is not retroactive, and there is no way to back-fill customers from responses collected while it was off. If you are unsure, leave it on — you can always turn it off later, but you cannot recover the gap.
Consolidating several website forms into one
If the same enquiry form appears on many pages, you do not need a TrueBeep form for each one. Build a single form with the union of the fields, mark the page-specific ones optional, and add a field that records where the submission came from — a Select of page slugs, plus a Textarea for the full URL.
Every page then posts to the same formId, differing only in that source value, and you get one inbox and one export you can still segment by page.
{
"responses": [
{ "key": "first_name", "value": "John" },
{ "key": "email_address", "value": "john.doe@example.com" },
{ "key": "inquiry_source", "value": "fiber" },
{ "key": "source_url", "value": "https://www.example.com/shop/internet/fiber?utm_source=newsletter" }
]
}
Make the source field a Select so its values stay consistent and filterable, and keep the full-URL field a Textarea — URLs with tracking parameters routinely exceed the 255-character varchar limit.
Webhooks
If the form has a webhook URL configured in its settings, TrueBeep POSTs the complete response — including every field's key, label, type, and value — to that URL after a successful submission. When a webhook secret is set it is sent as Authorization: Bearer <secret>.
The webhook target always comes from the form's own settings and can never be supplied in the request body. Delivery is fire-and-forget with a 10-second timeout: a failing webhook is logged but does not fail the submission.
Errors
code | Status | Meaning |
|---|---|---|
FORM_NOT_FOUND | 404 | No form with that id on your team |
RESPONSE_NOT_FOUND | 404 | No such response on that form |
FORM_NAME_TAKEN | 409 | Another form on your team already uses that name |
FIELDS_WOULD_BE_REMOVED | 409 | An update omitted existing fields; confirm with removeMissingFields |
NO_FORM_OWNER | 422 | The team has no existing form to inherit an owner from — create one in the dashboard first |
FORM_HAS_NO_FIELDS | 422 | The form exists but has no fields yet — finish it in the builder |
MISSING_REQUIRED_FIELDS | 400 | One or more required fields were absent. The offending keys are in fields |
INVALID_FIELD_VALUES | 400 | A value did not match its field's type, options, or length. Per-field messages are in fields |
AUTH_HEADER_MISSING | 401 | No Authorization header |
INVALID_CLIENT | 401 | The key does not resolve to a team |
API_KEY_EXPIRED | 401 | Generate a new key in team settings |
{
"success": false,
"code": "MISSING_REQUIRED_FIELDS",
"message": "Missing required field(s): first_name, email_address",
"fields": ["first_name", "email_address"]
}
Analytics API
The Analytics API exposes the social analytics behind your TrueBeep dashboard — post-level metrics and page/profile-level insights — for every connected platform: Facebook, Instagram, X (Twitter), LinkedIn, TikTok, and YouTube.
All endpoints are protected by ApiClientMiddleware and require the same secret key as the rest of the API. Use https://api.truebeep.com/v1 as the base URL, and pass your key as a bearer token:
Authorization: Bearer <token>
Requests are automatically scoped to the team that owns the API key — you only ever see your own team's analytics.
Response shape: normalized + raw
Every metrics payload carries two views so you never have to relearn each platform's vocabulary, yet never lose detail:
metrics/kpis— a normalized common set (impressions,reach,engagement,likes,comments,shares,clicks,videoViews,engagementRate). A metric that a platform does not report isnull(distinct from a measured0).rawMetrics/rawKpis— the platform's original keys, untouched (e.g. Facebook'spost_video_views_15s, LinkedIn'suniqueImpressionsCount).
null means "this platform does not provide this metric" or "not yet ingested" — it is not the same as 0. Data is only as fresh as the last analytics refresh; page/profile responses include lastRefreshedAt so you can surface staleness.
List Post Analytics
Returns paginated post-level analytics for a single platform. Post sub-types (feed post, reel, story) are mixed together by default and can be narrowed with postType.
{
"success": true,
"data": [
{
"id": "e8c43l37t1zayx8t66t5xbg9",
"postId": "1224...901",
"platform": "facebook",
"postType": "post",
"campaignId": null,
"content": "Summer sale is live!",
"permalinkUrl": "https://facebook.com/...",
"publishedAt": "2026-06-10T12:00:00.000Z",
"metrics": {
"impressions": 1200,
"reach": 900,
"engagement": 84,
"likes": 40,
"comments": 8,
"shares": 6,
"clicks": 30,
"videoViews": 210,
"engagementRate": 0.07
},
"rawMetrics": {
"post_impressions": 1200,
"post_impressions_unique": 900,
"post_video_views_15s": 150
}
}
],
"pagination": { "page": 1, "limit": 20, "total": 137 }
}
| Field | Type | Description |
|---|---|---|
platform(Required) | string | facebook, instagram, twitter, linkedin, tiktok, or youtube |
| Field | Type | Description |
|---|---|---|
postType(Optional) | string | post, reel, or story. Omit for all. A no-op on platforms without sub-types (e.g. Twitter) |
startDate(Optional) | string | ISO date; filters publishedAt >= |
endDate(Optional) | string | ISO date; filters publishedAt <= |
campaignId(Optional) | string | Filter to a single campaign |
search(Optional) | string | Case-insensitive match on post content |
page(Optional) | number | Default 1 |
limit(Optional) | number | Default 20, max 100 |
order(Optional) | string | asc or desc by publishedAt. Default desc |
startDate / endDate accept full ISO timestamps (2026-06-30T23:59:59Z) or plain dates (2026-06-30).
Get a Single Post
Returns the analytics for one post. Responds 404 if the post is not in your team or does not belong to {platform}. The data object has the same shape as a list element above.
{
"success": true,
"data": {
"id": "e8c43l37t1zayx8t66t5xbg9",
"postId": "1224...901",
"platform": "facebook",
"postType": "post",
"metrics": { "impressions": 1200, "engagement": 84, "engagementRate": 0.07 },
"rawMetrics": { "post_impressions": 1200 }
}
}
Profile / Page Analytics
Returns page/profile-level insights for a platform — headline KPIs, a time-series for the selected window, and period-over-period percentage change.
{
"success": true,
"data": {
"platform": "instagram",
"status": "ready",
"connected": true,
"lastRefreshedAt": "2026-07-13T04:00:00.000Z",
"window": "30d",
"kpis": {
"followers": 5400,
"impressions": 88000,
"reach": 61000,
"engagement": 4300
},
"rawKpis": { "Followers": 5400, "Reach": 61000 },
"timeSeries": { "impressions": [], "reach": [] },
"percentageChange": { "impressions": 0.12, "reach": -0.03 }
}
}
| Field | Type | Description |
|---|---|---|
window(Optional) | string | 7d, 30d, 90d, or 12m. Default 30d |
Profile metrics are stored as pre-aggregated windows (7d / 30d / 90d / 12m), so — unlike posts — they do not accept arbitrary date ranges. Pick the closest window.
Cross-Platform Summary
Returns one entry per platform (including ones you have not connected), ideal for a dashboard landing view in a single call.
{
"success": true,
"data": {
"window": "30d",
"platforms": [
{
"platform": "facebook",
"status": "ready",
"connected": true,
"lastRefreshedAt": "2026-07-13T04:00:00.000Z",
"kpis": { "followers": 12000, "impressions": 240000, "reach": 180000, "engagement": 9100 },
"postCount": 42
},
{
"platform": "tiktok",
"status": "not_connected",
"connected": false,
"lastRefreshedAt": null,
"kpis": null,
"postCount": 0
}
]
}
}
| Field | Type | Description |
|---|---|---|
window(Optional) | string | 7d, 30d, 90d, or 12m. Default 30d |
platforms(Optional) | string | Comma-separated filter, e.g. facebook,instagram. Omit for all |
Connection status
Because "not connected" is a normal state, profile and summary responses carry an explicit status instead of erroring:
status | connected | Meaning |
|---|---|---|
not_connected | false | The platform is supported but your team hasn't connected it |
pending | true | Connected, but analytics haven't been ingested yet |
unavailable | true | Connected, but analytics aren't available (e.g. a LinkedIn personal profile) |
ready | true | Connected and data is present |
An unsupported platform in the path (e.g. /analytics/social/myspace/posts) returns 404 PLATFORM_NOT_SUPPORTED. A supported-but-not-connected platform returns 200 with status: "not_connected".
Metric availability by platform
Not every platform reports every metric — unavailable ones are null in metrics. Notable differences:
| Platform | Notes |
|---|---|
Full post + page metrics; reach is unique impressions | |
reach, total_interactions (→ engagement), saves in raw | |
| Twitter/X | Uses public_metrics; reach/clicks not reported |
Metrics nested per post; engagementRate is a stored ratio; personal profiles have none | |
| TikTok | view_count drives impressions/reach/videoViews; no clicks |
| YouTube | Views drive impressions; channel KPIs come from account metadata |