VedLink External API Documentation Base URL: /api/external/v1

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.

Every API key is scoped to a single VedLink account. You cannot access data across multiple accounts with one key.

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:

HTTP Header
Authorization: Bearer vl_a3f9e2d1c8b7a6f5e4d3c2b1a0f9e8d7
API keys grant full access to the connected VedLink account. Store them securely (environment variables, secrets manager) — never hardcode them in source code or expose them in client-side applications.

Getting an API key

  1. The user logs into their VedLink portal
  2. Goes to API Keys in the sidebar
  3. Clicks + New Key, gives it a name and selects the integration type
  4. Copies the key — it is shown only once
  5. Pastes the key into your software's settings

Error response for invalid keys

401 Unauthorized
{
  "detail": "Invalid or inactive API key."
}

Rate Limits

Limits are applied per API key using a fixed window (resets every minute).

100
requests / minute
General
30
messages / minute
/messages/send-template/
300
contacts / minute
/contacts/sync/

Rate limit headers

Every response includes these headers:

HeaderDescription
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait (only present on 429 responses)

Rate limit exceeded response

429 Too Many Requests
{
  "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

200
Success
201
Resource created
207
Multi-status — partial success. Some items in a bulk request failed. Check the errors array.
400
Bad request — missing or invalid parameters
401
Unauthorized — API key missing, invalid, or inactive
404
Not found — template ID doesn't exist or belongs to another account
422
Unprocessable — WhatsApp is not connected for this account
429
Rate limit exceeded — slow down and retry after Retry-After seconds
502
WhatsApp API error — check the detail field for the upstream error

Sync 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.

POST /api/external/v1/contacts/sync/
Upsert contacts into the connected VedLink account. Each item is matched in order by external_id, then by phone/whatsapp (against either stored number), then by email. Creates a new contact if nothing matches.
Updating a single contact? Send a one-item 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

FieldTypeRequiredDescription
contactsarrayrequired Array of contact objects. Max 500 per request. Send a single-item array to update one contact.

Contact object

FieldTypeRequiredDescription
external_idstringoptional*Your internal customer ID. Used for reliable deduplication on repeat syncs. Highly recommended.
phonestringoptional*Phone number in E.164 format (e.g. +447911123456). Dedup key (matched against either stored number) and used for WhatsApp messaging.
whatsappstringoptional*WhatsApp number if different from phone. Defaults to phone when omitted. Also used as a dedup key.
emailstringoptional*Email address. Used as a dedup key (case-insensitive) when no ID/phone match is found.
namestringoptionalFull name. Split on first space into first name and last name.
birthdaystringoptionalDate in YYYY-MM-DD format.
anniversarystringoptionalDate in YYYY-MM-DD format.
citystringoptionalCity name.
custom_fieldsobjectoptionalKey→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
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"
    }
  ]
}
Always include 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.

GET /api/external/v1/custom-fields/
List all custom field definitions for the account.

Example request

curl
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
    }
  ]
}
POST /api/external/v1/custom-fields/
Bulk create/update field definitions. Idempotent — each field is matched by key (auto-derived from label when omitted), so re-sending the same definitions updates rather than duplicates them.

Field object

FieldTypeRequiredDescription
labelstringoptional*Human label shown in the UI, e.g. Ring Size.
keystringoptional*Stable JSON key, e.g. ring_size. Auto-derived from label (lowercase, underscores) when omitted.
typestringoptionalOne of text, number, date, boolean, select, email. Defaults to text on create.
requiredbooleanoptionalWhether the field must be filled when saving a customer.
optionsarrayoptionalAllowed values for type=select.
is_activebooleanoptionalInactive 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
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": []
}
Define your fields once on setup, then send their values per contact in custom_fields. Keys are matched exactly, so reuse the keys returned by 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.

GET /api/external/v1/templates/
Returns only templates with status 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
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"]
    }
  ]
}
The 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.

You usually don't need this. POST /messages/send-template/ takes a 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.
POST /api/external/v1/media/upload/
Uploads a file to the WhatsApp media API using the connected WhatsApp Business account. Returns a media_id you can pass to the send-template endpoint.

Request

Send as multipart/form-data (do not set Content-Type: application/json).

FieldTypeRequiredDescription
filefilerequiredAny 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
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
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

200
Upload successful — media_id returned.
400
file field missing from request.
422
WhatsApp not connected for this account.
502
WhatsApp media API rejected the upload — check token and file format.
Media IDs expire after ~30 days. Upload the file shortly before sending, or re-upload if you receive a "media not found" error from WhatsApp.

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.

POST /api/external/v1/messages/send-template/
Sends a WhatsApp template message using the connected WhatsApp Business account. The template_id must belong to this account and be in APPROVED status.

Request body

FieldTypeRequiredDescription
tostringrequiredRecipient phone number in E.164 format (e.g. +447911123456).
template_idintegerrequiredThe id returned by GET /templates/.
variablesobjectoptionalNamed key-value pairs for template placeholders. Keys must match the names in the template's variables list.
media_urlstringoptionalPublic https URL of the header file for an image, video or document template. WhatsApp downloads it directly — you don't upload anything.
media_idstringoptionalA media ID from POST /media/upload/, if you already uploaded the file. Takes precedence over media_url.
filenamestringoptionalFilename 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 haveSendWhat happens
A public URLmedia_urlWhatsApp fetches the file itself. Fastest — no upload, no media IDs, nothing expires.
A local filea file partSend the request as multipart/form-data; the file is uploaded for you in the same call.
The same file for many sendsmedia_idUpload 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.
The older field names — document_media_id, document_filename, image_link, image_id — still work exactly as before. Existing integrations need no changes.

Example — order ready notification

curl
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
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
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
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
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 24-hour rule: Template messages can be sent at any time. However, free-text replies (outside of this API) are only allowed within 24 hours of the customer's last message. Templates are the correct approach for outbound notifications.

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.

GET /api/external/v1/messages/send-template-url/
Queues the same template message as the POST endpoint. Identical validation, identical rate limit, identical result — only the transport differs.
Your API key travels in the URL. It will be recorded in browser history, server and proxy logs, and any analytics that sees the address. Only build these URLs inside software your staff controls — never put one in an email, a public page, or anywhere a customer could see it. Opening the URL sends the message immediately, so treat it as a live action, not a link to share. If a URL leaks, regenerate the key in VedLink portal → API Keys.

Query parameters

ParameterRequiredDescription
api_keyrequiredYour API key — the same one you would send as a Bearer token.
torequiredRecipient phone number, e.g. 919876543210.
template_idrequiredThe id from GET /templates/.
var_<name>optionalOne parameter per template variable — var_customer_name=Priya fills {{customer_name}}. Repeat for each one.
variablesoptionalAlternative to var_ parameters: the whole variables object as URL-encoded JSON. If you send this, var_ parameters are ignored.
media_urloptionalPublic https URL of the header file, for image, video and document templates. Must be URL-encoded — see the note below.
media_idoptionalA media ID from POST /media/upload/. Takes precedence over media_url.
filenameoptionalFilename shown to the recipient for document headers. Defaults to document.pdf.

Example

Line breaks are for readability — send it as one unbroken address:

URL
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

URL
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
URL-encode every value. Spaces become %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

200
Queued for sending.
400
to or template_id missing, variables not valid JSON, or media_url is not an http(s) address.
404
Template not found, not approved, or not yours — or the 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.
422
WhatsApp not connected for this account.
429
Rate limit exceeded — 30 messages/minute, counted together with the POST endpoint.
Everything else behaves exactly like POST /messages/send-template/ — the same templates, the same variable matching, the same AUTHENTICATION (OTP) handling, the same rate-limit budget. "queued": true means accepted for sending, not yet delivered.

Getting Started

  1. User onboards VedLink — they sign up at app.vedlink-ai.com and connect their WhatsApp Business account.
  2. Generate an API key — in VedLink portal → API Keys → + New Key. Name it and copy the key.
  3. Enter the key in your software — add a settings field where users can paste their VedLink API key. Store it securely.
  4. Sync contacts — on app startup or on a schedule, call POST /contacts/sync/ to push your customer list into VedLink.
  5. Fetch templates — call GET /templates/ to discover which message templates are available. Cache the result; refresh when needed.
  6. 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:

  1. Know the template_id for the relevant template (fetched from GET /templates/)
  2. Pass the correct variable values when sending

Recommended implementation

Pseudocode
// 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:

  1. Match by external_id (your customer ID) — most reliable
  2. Match by phone / whatsapp — against either stored number
  3. Match by email — case-insensitive
  4. Create a new contact if nothing matches
Always pass 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:

Pseudocode — batched sync
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