Create a template
curl --request POST \
--url https://lancepilot.com/api/v3/workspaces/{workspace}/templates \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "login_otp_template",
"language": "en",
"body": {
"text": "<string>",
"otp_code": "<string>",
"variables": [
"<string>"
]
},
"header": {
"type": "text",
"text": {
"content": "<string>",
"variables": [
"<string>"
]
}
},
"footer": "<string>",
"buttons": [
{
"text": "<string>",
"phone_number": "<string>",
"url": "<string>"
}
],
"unsubscribeButton": true,
"blockButton": true
}
'import requests
url = "https://lancepilot.com/api/v3/workspaces/{workspace}/templates"
payload = {
"name": "login_otp_template",
"language": "en",
"body": {
"text": "<string>",
"otp_code": "<string>",
"variables": ["<string>"]
},
"header": {
"type": "text",
"text": {
"content": "<string>",
"variables": ["<string>"]
}
},
"footer": "<string>",
"buttons": [
{
"text": "<string>",
"phone_number": "<string>",
"url": "<string>"
}
],
"unsubscribeButton": True,
"blockButton": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'login_otp_template',
language: 'en',
body: JSON.stringify({text: '<string>', otp_code: '<string>', variables: ['<string>']}),
header: {type: 'text', text: {content: '<string>', variables: ['<string>']}},
footer: '<string>',
buttons: [{text: '<string>', phone_number: '<string>', url: '<string>'}],
unsubscribeButton: true,
blockButton: true
})
};
fetch('https://lancepilot.com/api/v3/workspaces/{workspace}/templates', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://lancepilot.com/api/v3/workspaces/{workspace}/templates",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'login_otp_template',
'language' => 'en',
'body' => [
'text' => '<string>',
'otp_code' => '<string>',
'variables' => [
'<string>'
]
],
'header' => [
'type' => 'text',
'text' => [
'content' => '<string>',
'variables' => [
'<string>'
]
]
],
'footer' => '<string>',
'buttons' => [
[
'text' => '<string>',
'phone_number' => '<string>',
'url' => '<string>'
]
],
'unsubscribeButton' => true,
'blockButton' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://lancepilot.com/api/v3/workspaces/{workspace}/templates"
payload := strings.NewReader("{\n \"name\": \"login_otp_template\",\n \"language\": \"en\",\n \"body\": {\n \"text\": \"<string>\",\n \"otp_code\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n },\n \"header\": {\n \"type\": \"text\",\n \"text\": {\n \"content\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n }\n },\n \"footer\": \"<string>\",\n \"buttons\": [\n {\n \"text\": \"<string>\",\n \"phone_number\": \"<string>\",\n \"url\": \"<string>\"\n }\n ],\n \"unsubscribeButton\": true,\n \"blockButton\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://lancepilot.com/api/v3/workspaces/{workspace}/templates")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"login_otp_template\",\n \"language\": \"en\",\n \"body\": {\n \"text\": \"<string>\",\n \"otp_code\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n },\n \"header\": {\n \"type\": \"text\",\n \"text\": {\n \"content\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n }\n },\n \"footer\": \"<string>\",\n \"buttons\": [\n {\n \"text\": \"<string>\",\n \"phone_number\": \"<string>\",\n \"url\": \"<string>\"\n }\n ],\n \"unsubscribeButton\": true,\n \"blockButton\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://lancepilot.com/api/v3/workspaces/{workspace}/templates")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"login_otp_template\",\n \"language\": \"en\",\n \"body\": {\n \"text\": \"<string>\",\n \"otp_code\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n },\n \"header\": {\n \"type\": \"text\",\n \"text\": {\n \"content\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n }\n },\n \"footer\": \"<string>\",\n \"buttons\": [\n {\n \"text\": \"<string>\",\n \"phone_number\": \"<string>\",\n \"url\": \"<string>\"\n }\n ],\n \"unsubscribeButton\": true,\n \"blockButton\": true\n}"
response = http.request(request)
puts response.read_bodyTemplates
Add Template
POST
/
workspaces
/
{workspace}
/
templates
Create a template
curl --request POST \
--url https://lancepilot.com/api/v3/workspaces/{workspace}/templates \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "login_otp_template",
"language": "en",
"body": {
"text": "<string>",
"otp_code": "<string>",
"variables": [
"<string>"
]
},
"header": {
"type": "text",
"text": {
"content": "<string>",
"variables": [
"<string>"
]
}
},
"footer": "<string>",
"buttons": [
{
"text": "<string>",
"phone_number": "<string>",
"url": "<string>"
}
],
"unsubscribeButton": true,
"blockButton": true
}
'import requests
url = "https://lancepilot.com/api/v3/workspaces/{workspace}/templates"
payload = {
"name": "login_otp_template",
"language": "en",
"body": {
"text": "<string>",
"otp_code": "<string>",
"variables": ["<string>"]
},
"header": {
"type": "text",
"text": {
"content": "<string>",
"variables": ["<string>"]
}
},
"footer": "<string>",
"buttons": [
{
"text": "<string>",
"phone_number": "<string>",
"url": "<string>"
}
],
"unsubscribeButton": True,
"blockButton": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'login_otp_template',
language: 'en',
body: JSON.stringify({text: '<string>', otp_code: '<string>', variables: ['<string>']}),
header: {type: 'text', text: {content: '<string>', variables: ['<string>']}},
footer: '<string>',
buttons: [{text: '<string>', phone_number: '<string>', url: '<string>'}],
unsubscribeButton: true,
blockButton: true
})
};
fetch('https://lancepilot.com/api/v3/workspaces/{workspace}/templates', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://lancepilot.com/api/v3/workspaces/{workspace}/templates",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'login_otp_template',
'language' => 'en',
'body' => [
'text' => '<string>',
'otp_code' => '<string>',
'variables' => [
'<string>'
]
],
'header' => [
'type' => 'text',
'text' => [
'content' => '<string>',
'variables' => [
'<string>'
]
]
],
'footer' => '<string>',
'buttons' => [
[
'text' => '<string>',
'phone_number' => '<string>',
'url' => '<string>'
]
],
'unsubscribeButton' => true,
'blockButton' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://lancepilot.com/api/v3/workspaces/{workspace}/templates"
payload := strings.NewReader("{\n \"name\": \"login_otp_template\",\n \"language\": \"en\",\n \"body\": {\n \"text\": \"<string>\",\n \"otp_code\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n },\n \"header\": {\n \"type\": \"text\",\n \"text\": {\n \"content\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n }\n },\n \"footer\": \"<string>\",\n \"buttons\": [\n {\n \"text\": \"<string>\",\n \"phone_number\": \"<string>\",\n \"url\": \"<string>\"\n }\n ],\n \"unsubscribeButton\": true,\n \"blockButton\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://lancepilot.com/api/v3/workspaces/{workspace}/templates")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"login_otp_template\",\n \"language\": \"en\",\n \"body\": {\n \"text\": \"<string>\",\n \"otp_code\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n },\n \"header\": {\n \"type\": \"text\",\n \"text\": {\n \"content\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n }\n },\n \"footer\": \"<string>\",\n \"buttons\": [\n {\n \"text\": \"<string>\",\n \"phone_number\": \"<string>\",\n \"url\": \"<string>\"\n }\n ],\n \"unsubscribeButton\": true,\n \"blockButton\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://lancepilot.com/api/v3/workspaces/{workspace}/templates")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"login_otp_template\",\n \"language\": \"en\",\n \"body\": {\n \"text\": \"<string>\",\n \"otp_code\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n },\n \"header\": {\n \"type\": \"text\",\n \"text\": {\n \"content\": \"<string>\",\n \"variables\": [\n \"<string>\"\n ]\n }\n },\n \"footer\": \"<string>\",\n \"buttons\": [\n {\n \"text\": \"<string>\",\n \"phone_number\": \"<string>\",\n \"url\": \"<string>\"\n }\n ],\n \"unsubscribeButton\": true,\n \"blockButton\": true\n}"
response = http.request(request)
puts response.read_bodyOverview
Create a new WhatsApp message template. Templates must be approved by Meta before they can be used to send messages.Templates typically take 5-30 minutes for approval. Use the Sync endpoint to check approval status.
Request Body Schema
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique template name (only lowercase + underscores). |
language | string | Yes | Language code (e.g., en, es, fr). |
category | string | Yes | One of: MARKETING, UTILITY, AUTHENTICATION. |
header | object/null | No | Template header. Can be null, text, or media (image/video/document). |
body | object | Yes | Main message body. Supports variables ({{1}}, {{2}}). |
body.text | string | Yes | Message text (max 1024 chars). |
body.variables | array | Conditional | Required if variables exist in text. |
footer | string | No | Footer text (max 60 chars, no emojis). |
buttons | array | No | Interactive buttons (URL, Phone, Quick Reply). Max 3. |
unsubscribeButton | boolean | No | Show Unsubscribe button (MARKETING only). |
blockButton | boolean | No | Show Block button (MARKETING only). |
addSecurityRecommendation | boolean | Auth only | Include security warning (AUTHENTICATION only). |
codeExpirationMinutes | integer | Auth only | OTP validity period in minutes (1-1440, AUTHENTICATION only). |
copyCodeButton | boolean | Auth only | Add one-tap copy button (AUTHENTICATION only). |
Complete Examples
Example 1: Marketing Template with Image Header
{
"name": "summer_sale_promo",
"language": "en",
"category": "MARKETING",
"header": {
"type": "media",
"media": {
"type": "image",
"file": "<binary_file_data>"
}
},
"body": {
"text": "Hi {{1}}! 🎉\n\nOur Summer Sale is here! Get {{2}}% off on all items. Use code: {{3}}\n\nHurry, offer ends soon!",
"variables": ["John", "30", "SUMMER30"]
},
"footer": "Terms and conditions apply",
"buttons": [
{
"type": "URL",
"text": "Shop Now",
"url": "https://example.com/sale"
}
],
"unsubscribeButton": true
}
Example 2: Authentication (OTP) Template
{
"name": "login_otp_verification",
"language": "en",
"category": "AUTHENTICATION",
"addSecurityRecommendation": true,
"codeExpirationMinutes": 10,
"copyCodeButton": true
}
Authentication templates are auto-generated by Meta with standard security messaging. You only need to specify the three parameters above.
Example 3: Utility Template with Multiple Buttons
{
"name": "order_confirmation",
"language": "en",
"category": "UTILITY",
"header": {
"type": "text",
"text": {
"content": "Order #{{1}}",
"variables": ["12345"]
}
},
"body": {
"text": "Hi {{1}},\n\nYour order has been confirmed!\n\nOrder ID: {{2}}\nTotal: {{3}}\nDelivery: {{4}}",
"variables": ["Sarah", "ORD-12345", "$99.99", "Dec 15, 2025"]
},
"footer": "Thank you for shopping with us",
"buttons": [
{
"type": "URL",
"text": "Track Order",
"url": "https://example.com/track"
},
{
"type": "PHONE_NUMBER",
"text": "Contact Support",
"phone_number": "+1234567890"
},
{
"type": "QUICK_REPLY",
"text": "Cancel Order"
}
]
}
Example 4: Simple Text Template
{
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"body": {
"text": "Reminder: Your appointment with {{1}} is scheduled for {{2}} at {{3}}. Please arrive 10 minutes early.",
"variables": ["Dr. Smith", "December 15, 2025", "2:00 PM"]
},
"footer": "Reply CANCEL to reschedule"
}
Example 5: URL Button with Variable
{
"name": "personalized_offer",
"language": "en",
"category": "MARKETING",
"body": {
"text": "Exclusive offer for {{1}}! Your discount code {{2}} is ready. Click below to redeem.",
"variables": ["VIP Members", "VIP20"]
},
"buttons": [
{
"type": "URL",
"text": "Redeem Now",
"url": "https://example.com/offer?code={{1}}",
"variables": ["VIP20"]
}
]
}
Example 6: Video Header Template
{
"name": "product_tutorial",
"language": "en",
"category": "MARKETING",
"header": {
"type": "media",
"media": {
"type": "video",
"file": "<binary_video_data>"
}
},
"body": {
"text": "Watch this quick tutorial on {{1}}! Learn how to get the most out of your purchase in just {{2}} minutes.",
"variables": ["our new feature", "5"]
},
"footer": "Need help? Contact support"
}
Example 7: Document Header Template
{
"name": "invoice_delivery",
"language": "en",
"category": "UTILITY",
"header": {
"type": "media",
"media": {
"type": "document",
"file": "<binary_pdf_data>"
}
},
"body": {
"text": "Dear {{1}},\n\nPlease find your invoice #{{2}} attached. Amount due: {{3}}\n\nPayment due: {{4}}",
"variables": ["John Doe", "INV-001", "$150.00", "Dec 31, 2025"]
},
"footer": "Thank you for your business"
}
Validation Rules
Name Rules
Name Rules
- Must be unique in workspace
- Only lowercase letters and underscores
- Examples: ✅
welcome_message,order_confirmation_v2 - Invalid: ❌
WelcomeMessage,order-confirmation,template 1
Language Codes
Language Codes
Must be valid ISO language code:
- English:
en - Spanish:
es - French:
fr - German:
de - Portuguese:
pt - Arabic:
ar - And more…
Header Rules
Header Rules
Text Header:
- Max 60 characters
- Max 1 variable:
{{1}} - Cannot start/end with variable
- Example: ✅
Welcome {{1}} - Invalid: ❌
{{1}} Welcome
- Image: JPG/PNG, max 5MB (5120KB)
- Video: MP4, max 10MB (10240KB)
- Document: PDF, max 30MB (30720KB)
Body Rules
Body Rules
- Required field
- Max 1024 characters
- Cannot start or end with variable
- Max 2 consecutive newlines (
\n\nallowed,\n\n\nnot allowed) - Variables must match: if text has
{{1}}and{{2}}, variables array must have exactly 2 items - Variables are numbered sequentially:
{{1}},{{2}},{{3}}, etc.
Footer Rules
Footer Rules
- Optional field
- Max 60 characters
- No newlines allowed
- No emojis allowed
- Plain text only
Button Rules
Button Rules
- Max 3 buttons total (unsubscribe/block don’t count toward limit)
- URL Button:
- Requires
textandurl - Can have 1 variable at end:
https://example.com/page?id={{1}} - Variable must be in
variablesarray
- Requires
- PHONE_NUMBER Button:
- Requires
textandphone_number - Phone must include country code:
+1234567890
- Requires
- QUICK_REPLY Button:
- Requires
textonly - Max 3 quick replies per template
- Requires
- Button Order: URL → Phone → Quick Reply
Authentication Templates
Authentication Templates
- Only 3 fields required:
addSecurityRecommendation,codeExpirationMinutes,copyCodeButton - No custom header, body, footer, or buttons
- Meta auto-generates the template content
codeExpirationMinutesrange: 1-1440 (1 min to 24 hours)
Response
Success (201 Created)
{
"success": true,
"message": "Template created successfully and submitted to Meta for review.",
"data": {
"id": 1234,
"workspace_id": "9ca711b1-14ca-4bd5-9457-751db9c052ac",
"name": "welcome_template",
"language": "en",
"category": "MARKETING",
"status": "PENDING",
"provider_id": null,
"components": {
"header": { ... },
"body": { ... },
"footer": "...",
"buttons": [ ... ]
},
"created_at": "2025-12-03T10:00:00Z",
"updated_at": "2025-12-03T10:00:00Z"
}
}
Validation Error (422)
{
"success": false,
"message": "Validation failed",
"errors": {
"name": ["The name field must only contain lowercase letters and underscores."],
"body.variables": ["The number of variables must match the placeholders in the text."],
"header.media.file": ["The file size must not exceed 5120 kilobytes."]
}
}
Template Status Flow
After creation, templates go through this lifecycle:PENDING → (Meta Review 5-30 mins) → APPROVED or REJECTED
Common Validation Errors
| Error Message | Cause | Solution |
|---|---|---|
| ”not_start_or_end_with_var” | Text starts/ends with variable | Remove variable from start/end |
| ”only_one_variable” | Header has more than 1 variable | Use max 1 variable in header |
| ”Variable count mismatch” | Variables array doesn’t match placeholders | Count {{1}}, {{2}} in text and provide exact number |
| ”File too large” | Media exceeds size limit | Compress file (Image: 5MB, Video: 10MB, PDF: 30MB) |
| “Invalid phone number” | Missing country code | Add country code: +1234567890 |
| ”Template name exists” | Name already used | Choose unique name |
| ”no_more_than_two_consecutive_newlines” | Too many blank lines | Use max \n\n (2 newlines) |
Best Practices
Test Variables
Always test with actual variable values before submission
Keep It Simple
Start with basic templates, add complexity gradually
Follow Meta Guidelines
Avoid promotional language in UTILITY templates
Use Descriptive Names
Use clear names like
order_confirmation not template_1Optimize Media
Compress images/videos before upload
Plan Variables
Map out all dynamic values before creating template
Category Guidelines
- MARKETING
- UTILITY
- AUTHENTICATION
Purpose: Promotional messages, offers, announcementsCharacteristics:
- Can include unsubscribe/block buttons
- Subject to stricter Meta review
- Rate limits may apply
- Sales and promotions
- New product launches
- Event invitations
- Newsletter updates
Purpose: Transactional and account-related messagesCharacteristics:
- Faster approval
- Higher sending limits
- No promotional content
- Order confirmations
- Shipping updates
- Appointment reminders
- Account notifications
Purpose: One-time passwords and verification codesCharacteristics:
- Auto-generated by Meta
- Highest priority delivery
- Built-in security features
- Login OTPs
- Password reset codes
- Two-factor authentication
Example cURL Request
curl -X POST \
https://lancepilot.com/api/v3/workspaces/{workspace}/templates \
-H 'Authorization: Bearer YOUR_API_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"name": "welcome_template",
"language": "en",
"category": "UTILITY",
"body": {
"text": "Welcome {{1}}! Your account is now active.",
"variables": ["John"]
}
}'
Need More Examples?Check out TEMPLATE_PAYLOADS.md for 15+ comprehensive template examples covering all use cases.
Meta Review Time: Templates typically take 5-30 minutes for approval. Use the Sync endpoint to check status updates.
Next Steps
After creating a template:- Wait for Review: Meta reviews templates within 5-30 minutes
- Sync Status: Use POST /templates/sync
- Check Approval: Use GET /templates to verify status
- Send Messages: Once approved, use Send Template Message
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
ID of the workspace.
Body
application/json
Example:
"login_otp_template"
Example:
"en"
Available options:
MARKETING, UTILITY, AUTHENTICATION Show child attributes
Show child attributes
Option 2: Text Header
- Option 1
- Option 2
Show child attributes
Show child attributes
Maximum string length:
60Show child attributes
Show child attributes
Response
Template created successfully