Welcome to CRM School: structured lessons for Biz1 CRM, documents, and the App SDK.
1. Document PDF templates
Create your own HTML/CSS designs for document PDFs. When no custom template is selected (or set as default), Biz1 keeps using the existing PDF flow.
Where to manage templates
Open: Settings → Document PDF templates (/setting/document_pdf_templates).
From this page you can:
- Add a template
- Edit a template
- Test with mock data
- View the template list
- Set a default template
- Remove / archive a template
- Filter by document type
Supported document types
order_proposals
purchase_orders
invoice
receipt
receipt_tax_invoice
credit_invoice
delivery_invoice
proforma_invoice
detail_orders
gi_ir
organization_receipt
How a template is chosen when creating a document
Optional request fields:
| Field | Meaning |
|---|---|
pdf_template_id | Use this template (must belong to the user and match the document type) |
pdf_template_strict | If 1 and the template is invalid / render fails → return an error instead of falling back |
Behavior:
pdf_template_idempty → use the active default custom template (if one exists)- No default custom template → legacy / private PDF flow
- Valid
pdf_template_id→ use that template - Invalid id +
pdf_template_strict = 1→ error - Invalid id + strict not
1→ fall back to legacy PDF
Each saved document stores pdf_template_id and pdf_template_version_id so the PDF keeps the exact version used at create time.
Required placeholders (must exist before save / test)
| Required part | Accepted placeholders |
|---|---|
| Company name | {{company.company_name}}, {{company.name}}, {{owner.company_name}} |
| Company ID | {{company.company_tax_id}}, {{company.company_id}}, {{owner.company_tax_number}} |
| Company address | {{company.company_address}}, {{company.address}}, {{owner.address}} |
| Company phone | {{company.company_phone}}, {{company.phone}}, {{owner.phone}}, {{owner.mobile}} |
| Customer name | {{customer.name}} |
| Document date | {{document.date_created}}, {{document.date}} |
| Document ID | {{document.last_documents_id}}, {{document.id}} |
| Biz1 logo | {{crm_logo}} |
| Biz1 credit text | {{crm_credit}} |
Recommended credit block:
<a href="https://biz1.co.il">
<img src="{{crm_logo}}" width="48" />
<span>{{crm_credit}}</span>
</a>
Unsafe HTML/CSS is blocked. Missing required parts return a detailed validation error.
Main placeholders
Document
{{document.id}}
{{document.last_documents_id}}
{{document.date_created}}
{{document.date}}
{{document.due_date}}
{{document.type}}
{{document.lang}}
{{document.direction}}
{{document.coin}}
{{document.total}}
{{document.tax}}
{{document.discount}}
{{document.final_amount}}
{{document.note}}
{{document.note_html}}
{{document.payment_link}}
{{document.invoice_israel_code}}
Customer
{{customer.name}}
{{customer.email}}
{{customer.mobile}}
{{customer.phone}}
{{customer.address}}
{{customer.company}}
{{customer.corporation}}
{{customer.cf_name}}
Company / invoice settings
{{company.company_name}}
{{company.company_tax_id}}
{{company.company_address}}
{{company.company_phone}}
{{company.email}}
{{company.logo}}
{{company.company_letter}}
{{company.order_letter}}
{{company.receipt_letter}}
{{company.invoice_receipt_letter}}
{{company.purchase_order_letter}}
{{company.details_order_letter}}
Owner / user
{{owner.name}}
{{owner.email}}
{{owner.phone}}
{{owner.mobile}}
{{owner.website}}
{{owner.logo}}
{{owner.invoice_logo}}
{{owner.address}}
{{owner.company_name}}
{{owner.company_tax_number}}
Biz1
{{crm_logo}}
{{crm_credit}}
{{crm_credit}} is language-aware.
Payment / approval link
{{payment_link}}
{{document.payment_link}}
{{payment_title}}
{{payment_text}}
{{payment_link_label}}
When available:
- order_proposals — while the proposal is not approved / signed yet
- invoice, detail_orders, purchase_orders — when unpaid and the payment button is enabled
- Otherwise — empty string
Example:
{{#if payment_link}}
<section class="payment-cta">
<strong>{{payment_title}}</strong>
<span>{{payment_text}}</span>
<a href="{{payment_link}}">{{payment_link_label}}</a>
</section>
{{/if}}
{{payment_title}}, {{payment_text}}, and {{payment_link_label}} are language-aware.
Repeated header, footer, and page numbers
Default:
{{header}}
{{footer}}
Custom:
{{#header}}
<div style="font-family:Arial;font-size:9px;width:100%">
{{company.company_name}} - {{document.last_documents_id}}
<span style="float:right">{{page}} / {{pages}}</span>
</div>
{{/header}}
{{#footer}}
<div style="font-family:Arial;font-size:8px;width:100%">
<a href="https://biz1.co.il">{{crm_credit}}</a>
<span style="float:right">{{page}} / {{pages}}</span>
</div>
{{/footer}}
Page tokens (use only inside header/footer):
{{page}}
{{pages}}
{{total_pages}}
Header/footer markers are removed from the body and repeated on each page. Margins increase when header/footer are used.
RTL and language
Hebrew and other RTL documents get dir="rtl" / lang="he". English and Thai use LTR.
.document-pdf-template-root[dir=rtl] {
direction: rtl;
text-align: right;
}
.document-pdf-template-root[dir=ltr] {
direction: ltr;
text-align: left;
}
Notes and rich text
Escaped notes:
{{document.note}}
Safe rich HTML (triple braces):
{{{document.note_html}}}
{{{company.header_notes_order_proposal_html}}}
{{{company.order_letter_html}}}
{{{company.purchase_order_letter_html}}}
{{{company.details_order_letter_html}}}
Header notes (by document type), bottom letters, and multi-footer JSON fields are also available from company invoice settings — use the matching {{company.header_notes_*}}, {{company.*_letter}}, and {{company.multi_footer_notes_for_*}} placeholders.
Items, products, categories, and options
Loop all lines:
{{#items}}
<tr>
<td>{{item.name}}</td>
<td>{{item.qty}}</td>
<td>{{format_money(item.price, document.coin)}}</td>
<td>{{format_money(item.qty * item.price, document.coin)}}</td>
</tr>
{{/items}}
Item fields: name, qty, price, discount, description, notes, sku, discount_type, vat_include, line_total, options_text.
Also available per line: {{product.*}}, {{category.*}}.
Filter by category id:
{{#items_by_category 123}}
<tr>
<td>{{item.name}}</td>
<td>{{category.id}}</td>
<td>{{product.color}}</td>
</tr>
{{/items_by_category}}
Generic filter:
{{#items where category.id == 456}}
<tr>
<td>{{item.name}}</td>
<td>{{format_money(item.line_total, document.coin)}}</td>
</tr>
{{/items}}
Operators: == != > >= < <=
If blocks, math, and money
{{#if item.discount > 0}}
<td>{{item.discount}}</td>
{{else}}
<td>-</td>
{{/if}}
Rules:
- Single-level
{{#if}}only (do not nest if blocks) - Math inside one expression:
{{item.qty * item.price}} - Money:
{{format_money(item.line_total, document.coin)}} - Do not write
{{item.qty}}*{{item.price}}
Custom fields
Normalized:
{{customer.cf_name}}
{{product.cf_color}}
{{category.cf_floor_type}}
Exact helpers:
{{customer_cf("cf-name")}}
{{product_cf("color")}}
{{category_cf("floor-type")}}
Images and CSS
<img src="{{crm_logo}}" width="48" />
<img src="{{product.image}}" width="80" />
<img src="{{category.image}}" width="80" />
Images must come from trusted Biz1 hosts / upload paths.
Playwright renders modern CSS (grid, @page, etc.). Prefer print-friendly rules such as break-inside: avoid on line rows.
Blocked for safety: <script>, <iframe>, <object>, <embed>, <form>, JS event attributes, javascript: URLs, PHP tags, unknown external image hosts, external CSS imports, remote fonts that may fail to load.
Invoice Israel
{{document.invoice_israel_code}}
{{company.invoice_israel}}
{{company.force_tz}}
{{company.israel_default_data}}
Signatures (current limit)
There is no template placeholder for digital signatures / approval signature images. Custom HTML PDFs do not currently use the legacy TCPDF certificate-signing step.
Quick troubleshooting
| Problem | Check |
|---|---|
| Template not used | Ownership, matching document_type, status active, default set, or pdf_template_id passed |
| Missing fields error | All required placeholders including {{crm_logo}} and {{crm_credit}} |
| Payment link empty | Proposal not approved; invoice unpaid; payment button enabled |
| Header/footer not repeating | Use {{header}} / {{footer}} or block syntax; page tokens only inside them |
| Hebrew not RTL | document_lang=he + RTL CSS on .document-pdf-template-root |
Full technical reference: document-pdf-template-module-help.md
2. App SDK and realtime options
Build web, React, Vue, Flutter WebView, or Android WebView screens with the Biz1 JavaScript SDK. Define the domain once, login once, then reuse the same client for REST and realtime.
Live API help (every route): https://bull36.com/app/help
Setup
- Include
https://{domain}/app/sdk/biz1-sdk.js - Create one
Biz1SDK.Biz1Clientat startup withdomainand a storage object - Call
client.login()— the SDK stores the bearer token inbiz1_sdk_bearer_token - After login, call
client.account.basic()and cache user, organization, folders, statuses, team members, and field settings - Call routes with helpers (
client.customers.list()), dynamic helpers (client.routes.Customer.List()), orclient.request() - Keep list page size at 25 or less (
length/limit/per_page) - Pass JavaScript
Dateobjects or local datetime strings — the SDK posts UTCY-m-d H:i:s - Store ids from list/add responses and reuse them for get / update / remove / related calls
- On
401, the SDK clears the token — show login again
Include SDK
<script src="https://{domain}/app/sdk/biz1-sdk.js"></script>
Basic flow
const client = new Biz1SDK.Biz1Client({
domain: 'https://{user}.bull36.com',
storage: localStorage
});
await client.login({
username: 'USER EMAIL',
password: 'USER PASSWORD'
});
const user = await client.account.basic();
const customers = await client.customers.list({ folder_id: 1, length: 25 });
const total = await client.customers.count({ folder_id: 1 });
const single = await client.customers.get(
customers.rows[0].customer_id || customers.rows[0].id
);
Any route
const result = await client.request('Customer.List', {
folder_id: 1,
length: 25
});
const sameResult = await client.routes.Customer.List({
folder_id: 1,
length: 25
});
Date / time fields
await client.customers.add({
name: 'John Demo',
phone: '0500000000',
followup: new Date(2026, 6, 20, 10, 0, 0)
});
// Explicit UTC conversion helper:
const utcValue = Biz1SDK.toUtcDateTime(new Date(2026, 6, 20, 10, 0, 0));
How to call without the SDK helpers
POST /app/{Route.Name}
POST https://{user}.bull36.com/app/{Route.Name}
Authorization: Bearer YOUR TOKEN
Instructions:
- Login via
/app/Login(handle OTP if required) - Save the bearer token in secure app state
- Send
Authorization: Bearer …on every protected call - Send parameters as POST body fields
- Lists: max 25 rows; use the matching
.Countroute for totals - On create, store returned ids (
customer_id,mission_id, …) for later actions - On
401, clear token and return to login
Realtime (socket)
Use the socket after login to refresh open screens without polling. The same bearer token registers the socket device.
<script src="https://{domain}/realtime/socket.io/socket.io.js"></script>
<script src="https://{domain}/app/sdk/biz1-sdk.js"></script>
const socket = client.realtime.connect({
platform: 'web',
path: '/realtime/socket.io'
});
client.realtime.on('biz1:ready', function (payload) {
console.log('socket ready', payload.userId, payload.events);
});
client.realtime.on('*', function (event) {
console.log('realtime event', event.key, event.payload);
});
Prefer specific listeners for performance:
client.realtime.on('crm.lead.created', function (event) {
refreshCustomerList();
});
client.realtime.on('mission.created', function (event) {
refreshMissionList({ customer_id: event.payload.customer_id });
});
client.realtime.on('message.created', function (event) {
refreshCustomerMessages(event.payload.customer_id);
});
client.realtime.on('customer.followup', function (event) {
refreshCustomer(event.payload.customer_id);
});
Main event groups
- Customers / follow-up:
crm.lead.created,customer.updated,customer.followup,customer.restored - Messages / email:
chat.message.received,message.created,mission.message.created,project.message.created,message.mark_read,message.replied,message.forwarded,message.deleted,email.created,email.deleted - Missions / reminders:
mission.created,mission.updated,mission.done,mission.reopened,mission.deleted,mission.reminder,mission.open_popup,automation.reminder.popup - Meetings / appointments:
meeting.created,meeting.updated,meeting.deleted,automation.appointment.open_popup,appointment.created,appointment.updated,appointment.deleted - Calls:
call.incoming,call.status.popup - Tabs / statuses:
entries.created,entries.updated,entries.deleted,statuses.add.created,statuses.edit.updated,statuses.delete.deleted - Internal status: use
statuses.*whenpayload.data.typeisinternal_status
When a route response includes socket_event, use the same key in client.realtime.on(socket_event, handler).
Example realtime payloads
New customer
{
"key": "crm.lead.created",
"payload": {
"customer_id": 13745935,
"customer": {
"id": 13745935,
"name": "John Demo",
"email": "[email protected]",
"mobile": "0500000000"
}
}
}
Mission
{
"key": "mission.created",
"payload": {
"mission_id": 500,
"customer_id": 13745935
}
}
Building UIs with AI / prompts
Point builders at https://{domain}/app/help. Remind them that every date/time field must be sent as UTC Y-m-d H:i:s (the SDK converts local Date values automatically). Follow each route’s required/optional parameters and sample JSON from the help panel.