Runtime API Endpoint Reference

Graph-compatible message, media, template, health, profile, phone-settings, and QR endpoints for connection-scoped dh_live_ keys.

Base URL and Compatibility

https://api.dualhook.com/v25.0

Use Authorization: Bearer <DH_LIVE_KEY>. The supported paths intentionally match Meta Graph v25.0 so most integrations only change the host and credential. Never submit a Meta token or appsecret_proof; Dualhook applies both authorization and proof internally.

<PHONE_NUMBER_ID> and <WABA_ID> must exactly match the assets stored on the connection that owns the key. Object IDs are admitted only after connection/WABA ownership is proven.

Endpoint Matrix

OperationMethod and path
Send message/read/typing requestPOST /v25.0/<PHONE_NUMBER_ID>/messages
Upload mediaPOST /v25.0/<PHONE_NUMBER_ID>/media
Read media metadataGET /v25.0/<MEDIA_ID>
Download media bytesGET /v25.0/<MEDIA_ID>/content
Delete mediaDELETE /v25.0/<MEDIA_ID>
Read phone identity and healthGET /v25.0/<PHONE_NUMBER_ID>
Read WABA identity and healthGET /v25.0/<WABA_ID>
List/create/delete templatesGET, POST, or DELETE /v25.0/<WABA_ID>/message_templates
Read/update one templateGET or POST /v25.0/<TEMPLATE_ID>
Read/update business profileGET or POST /v25.0/<PHONE_NUMBER_ID>/whatsapp_business_profile
Read/update safe phone settingsGET or POST /v25.0/<PHONE_NUMBER_ID>/settings
List/create/update QR codesGET or POST /v25.0/<PHONE_NUMBER_ID>/message_qrdls
Read/delete QR codeGET or DELETE /v25.0/<PHONE_NUMBER_ID>/message_qrdls/<CODE>

Messages

Dualhook validates the bounded Graph message envelope and supports Meta's current message types, including text, media, templates, reactions, contacts, locations, interactive messages, Flows, catalogs, carousels, address messages, read status, and typing indicators. Meta-owned nested message fields pass through so newly documented message variants do not require a Dualhook release. Credential, Graph-host, and top-level asset-routing controls are always rejected.

curl -X POST "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/messages" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "12015550123",
    "type": "text",
    "text": {"body": "Hello"}
  }'
{
  "messaging_product": "whatsapp",
  "contacts": [{"input": "12015550123", "wa_id": "12015550123"}],
  "messages": [{"id": "wamid..."}]
}

Dualhook never retries a message mutation automatically.

Media

Upload uses the normal Graph multipart shape:

curl -X POST "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/media" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -F "messaging_product=whatsapp" \
  -F "file=@photo.jpg;type=image/jpeg"
{"id": "<MEDIA_ID>"}

Read metadata and bytes:

curl "https://api.dualhook.com/v25.0/<MEDIA_ID>" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

curl "https://api.dualhook.com/v25.0/<MEDIA_ID>/content" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  --output media.bin

Metadata uses the connection's phone ID internally. The returned url points back to Dualhook's /content route; a raw temporary Meta CDN URL is never exposed.

curl -X DELETE "https://api.dualhook.com/v25.0/<MEDIA_ID>" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

Uploaded media is immediately bound to the connection. Inbound webhook media is adopted only after Meta verifies access with the bound phone ID. An object already owned by another connection returns 403.

Phone and WABA Health

Use management webhooks for immediate changes. Poll this endpoint only as reconciliation, at most once every five minutes. Phone and WABA reads share a maximum of six requests per minute per connection and 30 per minute per WABA. Successful responses are cached for 60 seconds and include X-Dualhook-Cache: hit or miss. Cache hits also include the standard Age response header in seconds.

curl "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>?fields=id,display_phone_number,quality_rating,status,name_status,whatsapp_business_manager_messaging_limit,health_status" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

Phone fields are limited to:

id, display_phone_number, verified_name, code_verification_status, quality_rating, platform_type, is_on_biz_app, throughput, last_onboarded_time, is_official_business_account, account_mode, status, name_status, new_name_status, whatsapp_business_manager_messaging_limit, and health_status.

webhook_configuration is intentionally unavailable.

Meta's phone health_status already summarizes the related phone, WABA, business, and app health. Do not make a second WABA health request when that summary is all you need.

curl "https://api.dualhook.com/v25.0/<WABA_ID>?fields=id,name,message_template_namespace,account_review_status,business_verification_status,health_status" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

WABA fields are limited to id, name, currency, timezone_id, message_template_namespace, account_review_status, business_verification_status, health_status, and is_enabled_for_insights.

{
  "id": "<PHONE_NUMBER_ID>",
  "quality_rating": "GREEN",
  "status": "CONNECTED",
  "name_status": "APPROVED",
  "whatsapp_business_manager_messaging_limit": "TIER_10K"
}

Message Templates

List templates with an allowlisted field set:

curl "https://api.dualhook.com/v25.0/<WABA_ID>/message_templates?fields=id,name,language,status,category,rejected_reason,quality_score,health_status&status=APPROVED&limit=25" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

Supported read fields include identity, language, status, current/previous/correct category, rejection reason, components, quality score and reasons, health/can-send state, parameter/display format, send TTL, update time, link-tracking choice, and primary-device delivery state.

Template lists may be filtered by validated name, status, category, and language values.

Create:

curl -X POST "https://api.dualhook.com/v25.0/<WABA_ID>/message_templates" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "shipping_update",
    "language": "en_US",
    "category": "UTILITY",
    "components": [{
      "type": "BODY",
      "text": "Order {{1}} has shipped.",
      "example": {"body_text": [["A123"]]}
    }]
  }'
{"id": "<TEMPLATE_ID>", "status": "PENDING", "category": "UTILITY"}

Read and update an owned template:

curl "https://api.dualhook.com/v25.0/<TEMPLATE_ID>?fields=id,name,status,quality_score,rejected_reason" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

curl -X POST "https://api.dualhook.com/v25.0/<TEMPLATE_ID>" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"components":[{"type":"BODY","text":"Order {{1}} is on its way."}]}'

Template names and languages are immutable on the update route. For the first direct read of a template that has not yet been listed through Dualhook, include at least one template-specific field such as name, status, or language. Dualhook uses that hint to distinguish a template ID from a media ID, verifies the template against the selected WABA, and records only minimal ownership metadata.

Delete all languages with a name, one exact template with name plus hsm_id, or at most 100 verified IDs with a standalone hsm_ids parameter:

curl -X DELETE "https://api.dualhook.com/v25.0/<WABA_ID>/message_templates?name=shipping_update&hsm_id=<TEMPLATE_ID>" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

Business Profile

curl "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"
{
  "data": [{
    "business_profile": {
      "about": "Support Monday through Friday",
      "email": "support@example.com",
      "websites": ["https://example.com"],
      "vertical": "PROF_SERVICES"
    }
  }]
}
curl -X POST "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/whatsapp_business_profile" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"about":"Support Monday through Friday","websites":["https://example.com"]}'

Dualhook injects messaging_product. Updates may include about, address, description, email, profile_picture_handle, up to two websites, and a supported vertical. The app-scoped profile-image upload-session endpoint is not exposed.

For a Coexistence connection, read access remains available but profile changes must be made in the WhatsApp Business app. Runtime profile updates return 409.

Phone Settings and Calling

curl "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/settings" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

The response is projected to safe user_identity_change and calling configuration. include_sip_credentials is rejected, and SIP passwords, servers, keys, payload-encryption settings, and storage configuration are never returned.

Each update must contain exactly one feature:

curl -X POST "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/settings" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"user_identity_change":{"enable_identity_key_check":true}}'
curl -X POST "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/settings" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "calling": {
      "status": "ENABLED",
      "call_icon_visibility": "DEFAULT",
      "callback_permission_status": "ENABLED",
      "audio": {"additional_codecs": ["PCMA", "PCMU"]},
      "video": {"status": "DISABLED"}
    }
  }'

Success:

{"success": true}

Live call signaling (/calls), SDP, and call permissions are not part of this runtime surface. Calling-setting updates are not available for Coexistence connections and return 409; manage those settings in the WhatsApp Business app. Safe identity-change settings remain available.

QR Codes

List:

curl "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/message_qrdls?fields=code,prefilled_message,deep_link_url&limit=25" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

Read one code:

curl "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/message_qrdls/<CODE>" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

Create or update (include an existing 14-character code to update):

curl -X POST "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/message_qrdls" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"prefilled_message":"Tell me more","generate_qr_image":"SVG"}'
{
  "code": "4O4YGZEG3RIVE1",
  "prefilled_message": "Tell me more",
  "deep_link_url": "https://wa.me/message/4O4YGZEG3RIVE1"
}

Delete:

curl -X DELETE "https://api.dualhook.com/v25.0/<PHONE_NUMBER_ID>/message_qrdls/<CODE>" \
  -H "Authorization: Bearer <DH_LIVE_KEY>"

The prefilled message is limited to 140 characters. QR image format is PNG or SVG.

Pagination

Template and QR list routes accept limit plus one safe after or before cursor. Template limits are 1–100; QR limits are 1–25.

{
  "data": [],
  "paging": {
    "cursors": {
      "before": "<SAFE_CURSOR>",
      "after": "<SAFE_CURSOR>"
    }
  }
}

Dualhook strips Meta's raw next and previous URLs. Send only the cursor value back to the same Dualhook route:

GET /v25.0/<WABA_ID>/message_templates?limit=25&after=<SAFE_CURSOR>

Input and Size Limits

InputLimit
Message JSON64 KiB
Template create/update JSON128 KiB
Business profile JSON16 KiB
Settings JSON32 KiB
QR JSON4 KiB
Audio/video media16 MiB
Documents100 MiB
JPEG/PNG5 MiB
WebP500 KiB

JSON routes require Content-Type: application/json. Media upload requires a valid multipart boundary, messaging_product=whatsapp, and a final file part with a supported MIME type. The optional type part must match the file MIME type when supplied. Content-Length is optional; the streamed file is still stopped at its MIME-specific limit.

Errors

Runtime-owned errors use a stable JSON envelope:

{"error":{"message":"Unsupported query parameter: owner_business_info"}}

Important statuses:

  • 400 invalid schema, field, cursor, query, or asset-shaped request.
  • 401 invalid, revoked, expired, or quarantined dh_live_ credential.
  • 403 connection_not_routable valid key, blocked connection.
  • 403 asset_scope_conflict ambiguous active ownership.
  • 403 object ownership mismatch.
  • 409 operation unavailable for the connection mode, such as a Coexistence-only profile or calling setting.
  • 413 body or media limit exceeded.
  • 415 unsupported content type or MIME type.
  • 429 rate/failure-circuit limit, with Retry-After.
  • 502 invalid or oversized upstream response/redirect.
  • 503 runtime configuration or internal dependency unavailable.

Meta error codes, safe messages, and trace IDs remain useful, but credential-like fields and values are recursively sanitized. Meta error 190 also places the existing connection routing hold.

Unsupported Operations

The runtime deliberately excludes arbitrary Graph paths/versions/fields, webhook subscription or callback changes, app/business/partner/system-user/token management, other businesses' assets, registration/deregistration, phone/WABA deletion or migration, PIN and display-name changes, billing/funding, SIP credentials, encryption/storage settings, live calling, Flows management, certificates, catalogs, groups, contact-book management, marketing optimization, analytics, bulk template upsert/previews, and app-scoped upload sessions.

These operations are partner-wide, lifecycle-destructive, credential-bearing, cross-business, difficult to bind to one connection, or need a separate permission and ownership design.

Related

  • Runtime APIUse, protect, rotate, and scope dh_live_ keys for Dualhook's allowlisted Graph-compatible WhatsApp runtime.
  • Meta API Error ReferenceComplete WhatsApp Cloud API and Meta Graph error code reference — what each code means, where it appears, and how to fix it.
  • TemplatesTemplate lifecycle, categories, and management through Dualhook and Meta Graph API.
  • Business ProfilesManaging the public-facing WhatsApp business profile for your phone number.
Browse more docsStart Free Trial