VedLink External API
The VedLink External API enables your software to sync customer contacts and send WhatsApp messages on behalf of businesses that have connected their VedLink account to your application.
Each business user generates an API key from their VedLink portal (API Keys in the sidebar) and enters it into your software. Your software uses that key to make requests on their behalf.
Base URL
https://app.vedlink-ai.com/api/external/v1
Request format
All requests and responses use JSON. Set the Content-Type header to application/json for POST requests.
Authentication
Authenticate by including the API key in the Authorization header of every request:
Authorization: Bearer vl_a3f9e2d1c8b7a6f5e4d3c2b1a0f9e8d7
Getting an API key
- The user logs into their VedLink portal
- Goes to API Keys in the sidebar
- Clicks + New Key, gives it a name and selects the integration type
- Copies the key — it is shown only once
- Pastes the key into your software's settings
Error response for invalid keys
{
"detail": "Invalid or inactive API key."
}
Rate Limits
Limits are applied per API key using a fixed window (resets every minute).
Rate limit headers
Every response includes these headers:
| Header | Description |
|---|---|
| X-RateLimit-Remaining | Requests left in the current window |
| X-RateLimit-Reset | Unix timestamp when the window resets |
| Retry-After | Seconds to wait (only present on 429 responses) |
Rate limit exceeded response
{
"error": "Rate limit exceeded.",
"retry_after_seconds": 42
}
Error Reference
All error responses follow the same shape:
{
"error": "Human-readable description of what went wrong."
}HTTP status codes
errors array.Retry-After secondsdetail field for the upstream errorSync Contacts
Bulk-upsert up to 500 contacts in a single request. Safe to call repeatedly — duplicate detection runs automatically using the contact's external ID, phone/WhatsApp number, and email.
external_id, then by phone/whatsapp (against either stored number), then by email. Creates a new contact if nothing matches.
contacts array. Only the fields you include are changed — omitted fields are left as-is, and custom_fields are merged (your keys overwrite, the rest are kept). The response shows "updated": 1 when an existing contact matched, or "created": 1 if none did — so pass external_id (or an exact phone/email) to guarantee an update rather than a new record.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| contacts | array | required | Array of contact objects. Max 500 per request. Send a single-item array to update one contact. |
Contact object
| Field | Type | Required | Description |
|---|---|---|---|
| external_id | string | optional* | Your internal customer ID. Used for reliable deduplication on repeat syncs. Highly recommended. |
| phone | string | optional* | Phone number in E.164 format (e.g. +447911123456). Dedup key (matched against either stored number) and used for WhatsApp messaging. |
| string | optional* | WhatsApp number if different from phone. Defaults to phone when omitted. Also used as a dedup key. | |
| string | optional* | Email address. Used as a dedup key (case-insensitive) when no ID/phone match is found. | |
| name | string | optional | Full name. Split on first space into first name and last name. |
| birthday | string | optional | Date in YYYY-MM-DD format. |
| anniversary | string | optional | Date in YYYY-MM-DD format. |
| city | string | optional | City name. |
| custom_fields | object | optional | Key→value map for your account's custom fields. Keys must match a field defined via POST /custom-fields/; unknown keys are ignored and bad values are reported per-row. |
* At least one of external_id, phone, whatsapp or email is required per contact.
Example request
curl -X POST https://app.vedlink-ai.com/api/external/v1/contacts/sync/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "external_id": "JM-CUST-001", "name": "Priya Patel", "phone": "+447911123456", "whatsapp": "+447911123456", "email": "priya@example.com", "birthday": "1990-03-20", "anniversary": "2015-06-15", "city": "London", "custom_fields": { "ring_size": 14, "metal": "Gold" } }, { "external_id": "JM-CUST-002", "name": "Raj Sharma", "phone": "+447711234567" } ] }'
Response — 200 OK (all succeeded)
{
"created": 1, // new contacts created
"updated": 1, // existing contacts updated
"skipped": 0,
"errors": []
}Response — 207 Multi-Status (partial failure)
{
"created": 1,
"updated": 0,
"skipped": 0,
"errors": [
{
"external_id": "JM-CUST-002",
"phone": "+447711234567",
"error": "Description of what went wrong"
}
]
}external_id. On repeat syncs, this ensures the same contact is updated rather than duplicated — even if their phone number changes.
Custom Fields
Define and manage the custom customer fields for the connected account. These are the keys you can then populate via custom_fields on POST /contacts/sync/. The same fields appear in the VedLink portal's customer forms.
Example request
curl https://app.vedlink-ai.com/api/external/v1/custom-fields/ \
-H "Authorization: Bearer YOUR_API_KEY"
Response — 200 OK
{
"custom_fields": [
{
"key": "ring_size", // use this key in contacts.custom_fields
"label": "Ring Size",
"type": "number", // text | number | date | boolean | select | email
"required": false,
"options": [], // values for type=select
"is_active": true,
"order": 0
}
]
}key (auto-derived from label when omitted), so re-sending the same definitions updates rather than duplicates them.
Field object
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | optional* | Human label shown in the UI, e.g. Ring Size. |
| key | string | optional* | Stable JSON key, e.g. ring_size. Auto-derived from label (lowercase, underscores) when omitted. |
| type | string | optional | One of text, number, date, boolean, select, email. Defaults to text on create. |
| required | boolean | optional | Whether the field must be filled when saving a customer. |
| options | array | optional | Allowed values for type=select. |
| is_active | boolean | optional | Inactive fields stop showing in forms/lists but keep stored values. Defaults to true. |
* At least one of label or key is required per field. Max 100 fields per request.
Example request
curl -X POST https://app.vedlink-ai.com/api/external/v1/custom-fields/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fields": [ { "label": "Ring Size", "type": "number", "required": true }, { "label": "Metal", "type": "select", "options": ["Gold", "Silver", "Platinum"] } ] }'
Response — 200 OK
{
"created": 2,
"updated": 0,
"errors": []
}GET /custom-fields/.
List Templates
Retrieve all approved, active WhatsApp templates for the connected account. Use this to discover the template_id and required variable names before sending a message.
APPROVED that are active. Templates created in the VedLink portal and approved by Meta/WhatsApp appear here.
No request body or query parameters required.
Example request
curl https://app.vedlink-ai.com/api/external/v1/templates/ \
-H "Authorization: Bearer YOUR_API_KEY"
Response — 200 OK
{
"templates": [
{
"id": 42, // use this as template_id when sending
"template_name": "order_ready",
"slug": "order_ready", // WhatsApp template name
"category": "UTILITY", // UTILITY | MARKETING | AUTHENTICATION
"header_type": "none", // none | text | image | document | video
"content": "Dear {{customer_name}}, your order {{order_id}} is ready for collection.",
"variables": ["customer_name", "order_id"] // ordered list of variable names
},
{
"id": 17,
"template_name": "payment_reminder",
"slug": "payment_reminder",
"category": "UTILITY",
"header_type": "none",
"content": "Hi {{customer_name}}, your payment of {{amount}} is due on {{due_date}}.",
"variables": ["customer_name", "amount", "due_date"]
}
]
}variables array lists placeholders in the order they appear in the template. Pass values in this exact order when using /messages/send-template/.
Upload Media
Uploads a file once and returns a reusable media_id, valid for approximately 30 days.
media_url or the file itself and handles the upload for you in a single call. Use this endpoint only when you send the same file to many recipients and want to upload it once.
media_id you can pass to the send-template endpoint.
Request
Send as multipart/form-data (do not set Content-Type: application/json).
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | required | Any WhatsApp-supported media file. Images (JPEG, PNG), documents (PDF), video (MP4), and audio (OGG) are all accepted. The MIME type is detected automatically from the file. |
Example — upload a PDF invoice
curl -X POST https://app.vedlink-ai.com/api/external/v1/media/upload/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@/path/to/invoice.pdf"
Example — upload an image
curl -X POST https://app.vedlink-ai.com/api/external/v1/media/upload/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "file=@/path/to/banner.jpg"
Response — 200 OK
{
"media_id": "1351390050246714" // pass as media_id when sending
}Status codes
media_id returned.file field missing from request.Send Template Message
Send a Meta-approved WhatsApp template message to a phone number. Variables are matched by name to the placeholders in the template content.
template_id must belong to this account and be in APPROVED status.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | required | Recipient phone number in E.164 format (e.g. +447911123456). |
| template_id | integer | required | The id returned by GET /templates/. |
| variables | object | optional | Named key-value pairs for template placeholders. Keys must match the names in the template's variables list. |
| media_url | string | optional | Public https URL of the header file for an image, video or document template. WhatsApp downloads it directly — you don't upload anything. |
| media_id | string | optional | A media ID from POST /media/upload/, if you already uploaded the file. Takes precedence over media_url. |
| filename | string | optional | Filename shown to the recipient for document headers (e.g. Invoice_001.pdf). Defaults to document.pdf. |
Attaching media
A template whose header is an image, video or document needs a file with every message. Send it in whichever form you have it — VedLink puts it in the correct header slot based on the template's own header type, so there is nothing to configure per template:
| You have | Send | What happens |
|---|---|---|
| A public URL | media_url | WhatsApp fetches the file itself. Fastest — no upload, no media IDs, nothing expires. |
| A local file | a file part | Send the request as multipart/form-data; the file is uploaded for you in the same call. |
| The same file for many sends | media_id | Upload once via /media/upload/, then reference the ID. |
media_url must be reachable by WhatsApp's servers — a public URL with no login, no signed-session cookie and no IP allow-list. A link only your office network can open will fail to deliver. If your files aren't public, attach the file directly instead.
document_media_id, document_filename, image_link, image_id — still work exactly as before. Existing integrations need no changes.
Example — order ready notification
curl -X POST https://app.vedlink-ai.com/api/external/v1/messages/send-template/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+447911123456", "template_id": 42, "variables": { "customer_name": "Priya Patel", "order_id": "ORD-2024-881" } }'
Example — PDF invoice from a URL
One call. No upload step, no media IDs to keep track of:
curl -X POST https://app.vedlink-ai.com/api/external/v1/messages/send-template/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+447911123456", "template_id": 42, "variables": { "customer_name": "Priya Patel", "invoice_number": "INV-001", "amount": "5,000.00" }, "media_url": "https://yourapp.com/invoices/INV-001.pdf", "filename": "Invoice_INV-001.pdf" }'
Example — upload the file in the same call
When the file only exists on your machine, attach it as a file part. Because this is a form request, variables is sent as a JSON string:
curl -X POST https://app.vedlink-ai.com/api/external/v1/messages/send-template/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -F "to=+447911123456" \ -F "template_id=42" \ -F 'variables={"customer_name":"Priya Patel","invoice_number":"INV-001"}' \ -F "file=@/path/to/Invoice_INV-001.pdf"
Example — image header
Identical shape. The template's header type decides that this becomes an image, not a document:
curl -X POST https://app.vedlink-ai.com/api/external/v1/messages/send-template/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+447911123456", "template_id": 51, "variables": { "customer_name": "Priya Patel" }, "media_url": "https://yourapp.com/banners/diwali-offer.jpg" }'
Response — 200 OK
{
"queued": true,
"to": "+447911123456",
"template": "order_ready"
}How variables work
Variables are matched by name to the placeholders found in the template content in left-to-right order. Given the template:
// Template content "Dear {{customer_name}}, your order {{order_id}} is ready for collection." // Variables list (from GET /templates/) ["customer_name", "order_id"] // Your variables in the send request { "customer_name": "Priya Patel", "order_id": "ORD-2024-881" } // WhatsApp receives "Dear Priya Patel, your order ORD-2024-881 is ready for collection."
Authentication (OTP) templates
Templates with category = "AUTHENTICATION" deliver one-time passcodes. Pass the code as a single variable named code (aliases: 1, otp, otp_code). Maximum 15 characters. The code is automatically placed in both the message body and the copy-code button, as required by WhatsApp.
curl -X POST https://app.vedlink-ai.com/api/external/v1/messages/send-template/ \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+447911123456", "template_id": 57, "variables": { "code": "482913" } }'
Missing or over-long codes are rejected with a 400 before the message is queued.
WhatsApp not connected (422)
{
"error": "WhatsApp is not connected for this account. Please complete onboarding in the VedLink portal."
}The user needs to connect their WhatsApp Business account in the VedLink portal before messages can be sent.
Send via URL
The same send, with everything in the URL — no headers, no request body, no JSON. Use this when your software can only open a plain web address: a report tool, a spreadsheet HYPERLINK, a print-template button, a legacy application with no HTTP client. If you can make a normal POST request, use POST /messages/send-template/ instead.
Query parameters
| Parameter | Required | Description |
|---|---|---|
| api_key | required | Your API key — the same one you would send as a Bearer token. |
| to | required | Recipient phone number, e.g. 919876543210. |
| template_id | required | The id from GET /templates/. |
| var_<name> | optional | One parameter per template variable — var_customer_name=Priya fills {{customer_name}}. Repeat for each one. |
| variables | optional | Alternative to var_ parameters: the whole variables object as URL-encoded JSON. If you send this, var_ parameters are ignored. |
| media_url | optional | Public https URL of the header file, for image, video and document templates. Must be URL-encoded — see the note below. |
| media_id | optional | A media ID from POST /media/upload/. Takes precedence over media_url. |
| filename | optional | Filename shown to the recipient for document headers. Defaults to document.pdf. |
Example
Line breaks are for readability — send it as one unbroken address:
https://app.vedlink-ai.com/api/external/v1/messages/send-template-url/ ?api_key=YOUR_API_KEY &to=919876543210 &template_id=42 &var_customer_name=Priya%20Patel &var_invoice_number=INV-001
Example — with a PDF header
https://app.vedlink-ai.com/api/external/v1/messages/send-template-url/ ?api_key=YOUR_API_KEY &to=919876543210 &template_id=42 &var_customer_name=Priya%20Patel &media_url=https%3A%2F%2Fyourapp.com%2Finvoices%2FINV-001.pdf &filename=Invoice_INV-001.pdf
%20, and a media_url must be fully encoded — an unencoded file URL with its own &key=value parameters is read as extra parameters of this endpoint and the link arrives truncated. Most languages have a helper for this: encodeURIComponent in JavaScript, urllib.parse.quote in Python.
Response — 200 OK
{
"queued": true,
"to": "919876543210",
"template": "order_ready"
}Status codes
to or template_id missing, variables not valid JSON, or media_url is not an http(s) address.api_key is missing or invalid. An unrecognised key deliberately returns 404 rather than 401 so a leaked URL cannot confirm that a key exists."queued": true means accepted for sending, not yet delivered.
Getting Started
- User onboards VedLink — they sign up at app.vedlink-ai.com and connect their WhatsApp Business account.
- Generate an API key — in VedLink portal → API Keys → + New Key. Name it and copy the key.
- Enter the key in your software — add a settings field where users can paste their VedLink API key. Store it securely.
- Sync contacts — on app startup or on a schedule, call POST /contacts/sync/ to push your customer list into VedLink.
- Fetch templates — call GET /templates/ to discover which message templates are available. Cache the result; refresh when needed.
- Send messages — when a relevant event occurs, call POST /messages/send-template/.
Event-based Messages Guide
Since each account may have different message wording, templates are created in VedLink — not hardcoded in your software. Your software just needs to:
- Know the
template_idfor the relevant template (fetched from GET /templates/) - Pass the correct variable values when sending
Recommended implementation
// 1. On settings load: fetch & show available templates templates = GET /api/external/v1/templates/ show user a dropdown to select a template for this event type save selected template_id in your settings // 2. When event fires (e.g. order status changes) POST /api/external/v1/messages/send-template/ { "to": customer.phone, "template_id": settings.selected_template_id, "variables": { "customer_name": customer.name, "order_id": order.reference_number } }
Contact Sync Guide
When to sync
- Initial sync: push all customers when the user first connects
- Incremental sync: push only new/updated customers (daily or on change)
- On demand: add a "Sync to VedLink" button in your UI
Deduplication logic
VedLink deduplicates contacts using this priority:
- Match by
external_id(your customer ID) — most reliable - Match by
phone/whatsapp— against either stored number - Match by
email— case-insensitive - Create a new contact if nothing matches
external_id. It ensures a customer is updated — not duplicated — even if their phone number changes over time.
Batch size
Maximum 500 contacts per request. For larger syncs, batch your contacts and make multiple requests:
customers = getAllCustomers() // e.g. 2000 records for chunk in splitIntoChunksOf(customers, 500): POST /contacts/sync/ with { contacts: chunk } sleep(1 second) // be a good citizen