---
title: "WhatsApp API in PHP with cURL and Guzzle | D-API"
description: "Integrate the WhatsApp API in plain PHP with cURL or Guzzle, receive the webhook through php://input and run it all on shared hosting or in legacy systems."
url: https://d-api.cloud/en/whatsapp-api-php
---

By language

# WhatsApp API in PHP: cURL, Guzzle and a webhook on any host

To use the WhatsApp API in PHP you don't need a framework or a dedicated server: one cURL function sends the message and a file that reads php://input receives the webhook. It is the path for legacy systems and shared hosting, where you can't install a worker or open a port.

By D-API engineering teamUpdated Sep 24, 20266 min read

## The scenario: an old system on simple hosting

Plenty of back-office systems, stores and customer portals still run on procedural PHP, on shared hosting with cPanel and no root access. Rewriting them to add WhatsApp is not an option. The good news is that the D-API [WhatsApp API](https://d-api.cloud/en) is REST: it needs no library, no background process and no persistent WhatsApp connection on your server. D-API keeps the connection to the number; your PHP only makes HTTPS requests and receives HTTPS requests.

What the server needs:

-   the cURL extension enabled, which practically every host has;
-   outbound access to `https://api.d-api.cloud` on port 443;
-   a public HTTPS address for the webhook file;
-   access to the control panel's cron, if you want to process events in batches.

## Send a message with plain cURL

The function below fits in a `dapi.php` file and can be included from any screen in the system. It sets a maximum connect and response time, handles network failures and turns the API error response, which arrives as `{"success": false, "error": "...", "statusCode": 400}`, into an exception.

```
<?php
// dapi.php
define('DAPI_API_KEY', getenv('DAPI_API_KEY'));

function dapi_post(string $path, array $payload): array
{
    $ch = curl_init('https://api.d-api.cloud' . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 3,   // seconds to open the connection
        CURLOPT_TIMEOUT        => 10,  // seconds for the whole request
        CURLOPT_HTTPHEADER     => [
            'Authorization: ' . DAPI_API_KEY,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($payload),
    ]);

    $body = curl_exec($ch);
    if ($body === false) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException('Network failure talking to D-API: ' . $error);
    }

    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $json = json_decode($body, true) ?: [];

    if ($status >= 400) {
        throw new RuntimeException($json['error'] ?? 'HTTP ' . $status, $status);
    }
    return $json;
}

// usage
dapi_post('/api/v1/messages/send/text', [
    'sessionId' => 'store',
    'to'        => '14155550123',
    'text'      => 'We received your order 4812. Thank you!',
]);
```

By changing the path, the same function sends an image with a caption: use `/api/v1/messages/send/image` with the `image` field (public URL or base64) and `caption`. The timeout is what keeps the system's page from spinning forever if the network wobbles. On shared hosting PHP usually has a low execution limit, and a call without a timeout can take down the user's whole request.

## The same call with Guzzle

If the project already uses Composer, Guzzle makes the code more readable and clearly separates network errors from errors returned by the API.

```
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Exception\ConnectException;

$dapi = new Client([
    'base_uri'        => 'https://api.d-api.cloud',
    'timeout'         => 10,
    'connect_timeout' => 3,
    'headers'         => ['Authorization' => getenv('DAPI_API_KEY')],
]);

try {
    $dapi->post('/api/v1/messages/send/text', [
        'json' => ['sessionId' => 'store', 'to' => '14155550123', 'text' => 'Your order has shipped.'],
    ]);
} catch (BadResponseException $e) {
    $error = json_decode((string) $e->getResponse()->getBody(), true);
    error_log('D-API rejected: ' . ($error['error'] ?? $e->getMessage()));
} catch (ConnectException $e) {
    error_log('D-API unreachable or timed out: ' . $e->getMessage());
}
```

`BadResponseException` covers 4xx and 5xx responses. `ConnectException` covers DNS, refused connections and timeouts. For the first, review the payload; for the second, it is worth retrying later.

## Receive the webhook with php://input

The webhook arrives as a POST with a JSON body. In PHP that means reading `php://input`: `$_POST` is empty because it is not a form. On shared hosting, the safest way to handle events is to store first and process later, because the file has to respond fast and the provider's PHP-FPM usually has few processes available.

```
<?php
// whatsapp-webhook.php  (registered URL: https://yoursite.com/whatsapp-webhook.php?token=SECRET)
require __DIR__ . '/config.php';

if (!hash_equals(WEBHOOK_TOKEN, $_GET['token'] ?? '')) {
    http_response_code(401);
    exit;
}

$raw   = file_get_contents('php://input');
$event = json_decode($raw, true);
if (!is_array($event) || !isset($event['event'])) {
    http_response_code(400);
    exit;
}

// a unique key avoids storing the same retried event twice
$key = $event['data']['id'] ?? $event['traceId'];
$pdo->prepare(
    'INSERT IGNORE INTO whatsapp_events (event_key, event, session, payload, created_at)
     VALUES (?, ?, ?, ?, NOW())'
)->execute([$key, $event['event'], $event['sessionId'], $raw]);

http_response_code(200);
echo 'ok';
```

The `whatsapp_events` table needs a unique index on `event_key`; that is what makes `INSERT IGNORE` drop repeats. The webhook has no signature, so the token in the query string is the endpoint's protection. A cron script running every minute reads pending events and does the slower work: replying to the customer, updating the order, notifying the finance team.

In that script, mark each row as processed only after the work is done, and cap how many rows it takes per run, for example 100. That way a backlog does not blow past the maximum execution time your provider sets for PHP.

## Common pitfalls on shared servers

| Symptom | Likely cause | What to do |
| --- | --- | --- |
| Events stop arriving after a site change | An .htaccess rule redirecting the file and returning 404 | Exclude the webhook path from rewrite rules. 404 and 410 responses stop the retries |
| Webhook gets a 403 for no apparent reason | The provider's web application firewall blocking POST with JSON | Ask support for an exception for that file |
| Same message processed twice | A slow response caused the delivery to be repeated | Store and respond, process in cron, unique index |
| Sending fails with a certificate error | Outdated CA bundle on the server | Update PHP or the certificate bundle; never turn off SSL verification |

## Where this fits in your system

The most common entry points in older PHP systems are order checkout, invoice issuance and scheduling. A call to `dapi_post` right after the `INSERT` that already exists covers most of it. For a payment reminder flow, see the [payment reminders](https://d-api.cloud/en/whatsapp-payment-reminders) guide. If your project runs on Laravel, the [Laravel guide](https://d-api.cloud/en/whatsapp-api-laravel) shows the version with queues and notifications. Details of each incoming event are in [WhatsApp webhooks](https://d-api.cloud/en/whatsapp-webhook).

The connection used in these examples is the unofficial one, paired by QR code, which does not require template approval to send the first message. The rules and precautions for that model are in [unofficial WhatsApp API](https://d-api.cloud/en/unofficial-whatsapp-api), and the plans, billed per connection, on the [pricing](https://d-api.cloud/en/pricing) page.

## Frequently asked questions

### Does it work on shared hosting?

In most cases, yes. Sending only needs the cURL extension and outbound HTTPS. The webhook is a regular PHP file reachable from the internet. Check with your provider that outbound requests are not blocked.

### Do I need Composer?

No. The cURL example uses only native PHP functions and fits in a single file included with require. Composer only comes in if you want Guzzle or already use it elsewhere in the project.

### What is the minimum PHP version?

The examples on this page run on PHP 7.4 or later. If the system is still on an older version, the logic is the same, but you will need to remove the type declarations from the functions.

### My webhook receives the POST but $\_POST is empty. Why?

Because the body arrives as JSON, and PHP only fills $\_POST for forms. Read the raw body with file\_get\_contents on php://input and decode it with json\_decode.

### Can I use the same code in Laravel?

You can, but Laravel already has the Http client, queues and notifications that make the integration cleaner. There is a dedicated Laravel guide for that case.

Try D-API's WhatsApp API3-day free trial, no credit card.[Start free trial](https://app.d-api.cloud/register?lang=en)

## Keep reading

-   [By languageWhatsApp API in Laravel](https://d-api.cloud/en/whatsapp-api-laravel)
-   [By languageWhatsApp API in Java](https://d-api.cloud/en/whatsapp-api-java)
-   [By languageWhatsApp API in C# and .NET](https://d-api.cloud/en/whatsapp-api-csharp)
-   [By languageWhatsApp API in Go](https://d-api.cloud/en/whatsapp-api-golang)

## Try D-API's WhatsApp API

3-day trial with full access. No credit card, no lock-in.

[Start free trial](https://app.d-api.cloud/register?lang=en)[Read the docs](https://docs.d-api.cloud/)
