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
| Operation | Method and path |
|---|---|
| Send message/read/typing request | POST /v25.0/<PHONE_NUMBER_ID>/messages |
| Upload media | POST /v25.0/<PHONE_NUMBER_ID>/media |
| Read media metadata | GET /v25.0/<MEDIA_ID> |
| Download media bytes | GET /v25.0/<MEDIA_ID>/content |
| Delete media | DELETE /v25.0/<MEDIA_ID> |
| Read phone identity and health | GET /v25.0/<PHONE_NUMBER_ID> |
| Read WABA identity and health | GET /v25.0/<WABA_ID> |
| List/create/delete templates | GET, POST, or DELETE /v25.0/<WABA_ID>/message_templates |
| Read/update one template | GET or POST /v25.0/<TEMPLATE_ID> |
| Read/update business profile | GET or POST /v25.0/<PHONE_NUMBER_ID>/whatsapp_business_profile |
| Read/update safe phone settings | GET or POST /v25.0/<PHONE_NUMBER_ID>/settings |
| List/create/update QR codes | GET or POST /v25.0/<PHONE_NUMBER_ID>/message_qrdls |
| Read/delete QR code | GET 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
| Input | Limit |
|---|---|
| Message JSON | 64 KiB |
| Template create/update JSON | 128 KiB |
| Business profile JSON | 16 KiB |
| Settings JSON | 32 KiB |
| QR JSON | 4 KiB |
| Audio/video media | 16 MiB |
| Documents | 100 MiB |
| JPEG/PNG | 5 MiB |
| WebP | 500 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:
400invalid schema, field, cursor, query, or asset-shaped request.401invalid, revoked, expired, or quarantineddh_live_credential.403 connection_not_routablevalid key, blocked connection.403 asset_scope_conflictambiguous active ownership.403object ownership mismatch.409operation unavailable for the connection mode, such as a Coexistence-only profile or calling setting.413body or media limit exceeded.415unsupported content type or MIME type.429rate/failure-circuit limit, withRetry-After.502invalid or oversized upstream response/redirect.503runtime 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.