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>
Upload a template header samplePOST /v25.0/<WABA_ID>/message_template_media
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, typing indicators, and BSUID-only sends using recipient instead of to. 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..."}]
}

For a user whose phone number is unavailable, omit to and send the BSUID or parent BSUID through recipient:

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "recipient": "CO.2000162364208387",
  "type": "text",
  "text": {"body": "Hello"}
}

Every send requires type and either to or recipient. If both identifiers are present, Dualhook forwards both and Meta gives to precedence. Dualhook treats recipient as a bounded opaque identifier so Meta can evolve its syntax without requiring a Runtime API release, and does not store or log it.

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.

Meta's optional phone_number_id scoping parameter is accepted on the metadata and delete routes. It must equal the connection's stored phone-number ID or the request returns 403; Dualhook applies that ID internally either way, so the parameter can also be omitted.

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.

For an IMAGE, VIDEO, or DOCUMENT header, first upload one sample file. This Dualhook-owned WABA route performs Meta's two-step resumable upload internally and returns only the resulting template header handle:

curl -X POST \
  "https://api.dualhook.com/v25.0/<WABA_ID>/message_template_media?file_length=12345&file_type=image%2Fjpeg" \
  -H "Authorization: Bearer <DH_LIVE_KEY>" \
  -H "Content-Type: image/jpeg" \
  --data-binary "@header.jpg"
{"h":"<HEADER_HANDLE>"}

file_length is the exact raw file size in bytes and must match the body. file_type must exactly match Content-Type. Supported samples are JPEG/PNG (5 MiB), MP4/3GPP (16 MiB), and PDF (100 MiB). Dualhook creates the app-scoped upload session internally; callers cannot provide an app ID, upload-session ID, offset, token, proof, or Graph host.

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": "HEADER",
        "format": "IMAGE",
        "example": {"header_handle": ["<HEADER_HANDLE>"]}
      },
      {
        "type": "BODY",
        "text": "Order {{1}} has shipped.",
        "example": {"body_text": [["A123"]]}
      }
    ]
  }'
{"id": "<TEMPLATE_ID>", "status": "PENDING", "category": "UTILITY"}

The uploaded sample is for Meta's template review. When sending the approved template through /messages, supply the actual per-message media as a URL or phone-scoped media ID in the template header parameter.

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, calling, and read-only storage_configuration fields. storage_configuration reports Meta Local Storage for data-residency evidence ({"status":"DEFAULT"} when it was never enabled, IN_COUNTRY_STORAGE_ENABLED plus data_localization_region once enabled). include_sip_credentials is rejected, and SIP passwords, servers, keys, and payload-encryption settings are never returned.

Each update must contain exactly one feature. storage_configuration is not writable through the Runtime API because Meta only accepts it on a deregistered number; see Local Storage.

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
Template header JPEG/PNG sample5 MiB
Template header MP4/3GPP sample16 MiB
Template header PDF sample100 MiB
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.

Template header sample upload uses the raw file body rather than multipart. Both file_length and file_type are required, Content-Type must match, and the streamed body must contain exactly the declared number of bytes.

Errors

Runtime-owned errors use a stable JSON envelope. For a symptom-first walkthrough of each one, see Runtime API Errors.

{"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 or failure-circuit limit, with Retry-After; this includes repeated authentication failures for one credential, returned in place of 401 once that credential is throttled. Cached revoked/expired credentials continue returning 401 without a database authentication lookup for up to 24 hours.
  • 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 caller-controlled app-scoped upload sessions. Template header samples are supported only through the WABA-bound one-step route above.

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