Skip to content

feat(api): add WhatsApp send route endpoint#215

Open
Gill87 wants to merge 10 commits into
benevolentbandwidth:mainfrom
Gill87:feat/whatsapp-send-route
Open

feat(api): add WhatsApp send route endpoint#215
Gill87 wants to merge 10 commits into
benevolentbandwidth:mainfrom
Gill87:feat/whatsapp-send-route

Conversation

@Gill87

@Gill87 Gill87 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds the C++ backend endpoint for sending optimized route messages through WhatsApp.
  • Keeps WhatsApp credentials server-side and forwards successful Meta Graph API responses back to callers.

Motivation

Changes

  • Adds POST /api/whatsapp/send-route with request validation, credential checks, and Meta Graph API POST handling.
  • Wires the endpoint into API startup and CMake build sources.
  • Adds local HTTP integration tests for invalid payloads, missing configuration, successful upstream forwarding, and upstream failure mapping.

Validation

  • git diff --check
  • bash -n tests/integration/http_server/whatsapp_send_route_forwards_to_upstream_test.sh tests/integration/http_server/whatsapp_send_route_rejects_invalid_request_test.sh tests/integration/http_server/whatsapp_send_route_requires_configuration_test.sh tests/integration/http_server/whatsapp_send_route_returns_502_for_upstream_failure_test.sh
  • cmake --preset dev blocked locally because CMake could not find jsoncppConfig.cmake / jsoncpp-config.cmake.
  • clang-format -i app/api/src/endpoints/whatsapp_endpoint.cpp app/api/include/deliveryoptimizer/api/endpoints/whatsapp_endpoint.hpp blocked locally because clang-format is not installed in this shell.

Risk

  • Low to moderate: the endpoint is additive, but production success depends on Cloud Run providing valid WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID secrets.
  • The upstream base URL defaults to Meta Graph API and is overridable only for local/stubbed tests.

Rollout and Recovery

  • Deploy normally after CI passes and manually verify on Cloud Run where WhatsApp secrets are injected.
  • Roll back the deployment if WhatsApp sends fail or remove the frontend call path until credentials/upstream behavior are corrected.

@markboenigk markboenigk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Good structure and test coverage — the four integration test scripts cover the happy path, validation errors, missing credentials, and upstream failure cleanly. Three issues need addressing before merge.


🔴 Whitespace env var silently bypasses the 503 guard

app/api/src/endpoints/whatsapp_endpoint.cpp lines 104–106

ResolveEnvOrDefault only checks for a null or zero-length string. A value like " " (a trailing newline from Secret Manager, or a space from a copy-paste into Cloud Run's env-var field) is not .empty(), so the access_token.empty() || phone_number_id.empty() guard evaluates false. The handler proceeds and sends Authorization: Bearer to Meta, which returns error 190. The caller gets a 502 "WhatsApp upstream request failed" with no hint the service is misconfigured — it looks like a transient Meta outage.

Fix: Apply the same std::all_of(…std::isspace…) check already used in ReadNonEmptyStringMember when reading the credentials, or trim the env var values before the empty check.


🔴 Graph API version v18.0 is hardcoded with no override

app/api/src/endpoints/whatsapp_endpoint.cpp line 119

upstream_request->setPath("/v18.0/" + phone_number_id + "/messages");

The base URL gets ResolveNormalizedUrlEnvOrDefault so it can be swapped for tests; the API version cannot be changed without a code edit and redeploy. Meta v18.0 was released September 2023 on a 2-year deprecation cycle — its end-of-life is approximately September 2025. Once Meta enforces the sunset for this app, every send will return OAuthException code 2635 and WhatsApp notifications fail permanently with no runtime fix.

Fix: At minimum, extract a named constant (constexpr char kWhatsAppApiVersion[] = "v18.0"). Ideally add a WHATSAPP_API_VERSION env-var override consistent with kWhatsAppBaseUrlEnv, and bump the default to the current stable version (v21.0+).


🟡 Any caller who can reach the Cloud Run URL can trigger sends

app/api/src/endpoints/whatsapp_endpoint.cpp line 89

There is no application-layer auth on this handler — no Drogon filter, no token check in the lambda. This is consistent with the rest of the endpoints in this server, but the external impact here is qualitatively different: a POST from any caller sends a real WhatsApp message from the business's registered number to an arbitrary phone number, spending Meta API quota. Whether Cloud Run IAM is currently blocking allUsers is not visible from the code.

Worth confirming the Cloud Run service requires authentication (roles/run.invoker not granted to allUsers) before deploying, or adding an HMAC/shared-secret check on this endpoint specifically.


🔵 Nit: phone_number_id is not whitespace-trimmed before URL construction

app/api/src/endpoints/whatsapp_endpoint.cpp line 119

A trailing newline in WHATSAPP_PHONE_NUMBER_ID (common in Secret Manager exports) produces a malformed HTTP request line that Meta silently rejects. A whitespace trim on the env var value (same fix as the first finding above) covers this case.

@Gill87

Gill87 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Hey Mark,

Thank you for your feedback!

I went ahead and pushed a commit to address your concerns. The only thing I could not verify is whether the Cloud Run service requires authentication (roles/run.invoker not granted to allUsers) before deploying. Can you check this on your end?

@markboenigk

Copy link
Copy Markdown
Collaborator

Thanks for the fixes — verified all three:

  • Whitespace env var bypassing the 503 guard: fixed (TrimWhitespace + ResolveTrimmedEnvOrDefault).
  • Hardcoded v18.0 API version: fixed (WHATSAPP_API_VERSION override, default bumped to v23.0).
  • phone_number_id not trimmed: fixed (same trim path covers it).

One item still open — the app-layer auth question. I checked: the Cloud Run service (deliveryoptimizer, us-east1) currently has roles/run.invoker granted to allUsers, so it's publicly invokable. Since that's a service-wide IAM setting and other endpoints on this service need to stay public, locking down IAM isn't the right fix here — we need a lightweight shared-secret check scoped to just this endpoint.

I've already created WHATSAPP_SEND_ROUTE_SECRET in Secret Manager (b2-delivery-optimizer project) and wired it into the Cloud Run service as an env var, so it's ready to read. Could you add the following to whatsapp_endpoint.cpp?

// near the other constexpr declarations:
constexpr char kWhatsAppSendSecretEnv[] = "WHATSAPP_SEND_ROUTE_SECRET";
constexpr char kSendSecretHeader[] = "X-WhatsApp-Send-Secret";

// inside the handler, right after the request_body null check (before reading to/message):
const auto configured_secret = ResolveTrimmedEnvOrDefault(kWhatsAppSendSecretEnv, "");
if (configured_secret.empty()) {
  std::move(callback)(
      BuildErrorResponse(drogon::k503ServiceUnavailable, "WhatsApp is not configured."));
  return;
}

const auto provided_secret = request->getHeader(kSendSecretHeader);
if (provided_secret != configured_secret) {
  std::move(callback)(BuildErrorResponse(drogon::k401Unauthorized, "Unauthorized."));
  return;
}

This reuses your existing ResolveTrimmedEnvOrDefault helper (so it's whitespace-safe too) and fails closed the same way the existing credential check does. On the frontend side, whatsappClient.ts will need to send this header once it's updated to call the real endpoint instead of mocking — I'll track that separately.

Add a WHATSAPP_SEND_ROUTE_SECRET check on the send-route endpoint so it
fails closed (503) when unconfigured and rejects requests (401) missing
a matching X-WhatsApp-Send-Secret header, since the Cloud Run service is
publicly invokable. Update existing integration tests to supply the
secret and add a dedicated test for the new auth behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Gill87

Gill87 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Hey Mark,
I added a secret check in the code that returns a 503 if the secret is empty and a 401 if the secret does not match the configured Cloud Run secret. I also added test coverage for this change, although, the frontend mock data endpoint remains the same for now, since you mentioned that you would like to track that seperately.

@Gill87 Gill87 requested a review from markboenigk July 12, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants