> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loyalty.lt/llms.txt
> Use this file to discover all available pages before exploring further.

# SMS Webhooks

> Receive real-time SMS delivery status updates via webhooks

# SMS Webhooks

Webhooks allow you to receive real-time notifications when SMS message status changes. Instead of polling the status endpoint, you can configure a callback URL to receive automatic updates.

## How Webhooks Work

<Steps>
  <Step title="Send SMS with Webhook">
    Include `receipt: true` and `receiptURL` when sending an SMS. Both are required to receive webhooks.
  </Step>

  <Step title="Message Status Changes">
    When the message is sent, delivered, or fails, our system triggers a webhook.
  </Step>

  <Step title="Receive POST Request">
    We send a POST request to your webhook URL with the delivery status.
  </Step>

  <Step title="Confirm Receipt">
    Your server should respond with HTTP 200 to confirm receipt.
  </Step>
</Steps>

## Enabling Webhooks

### Setting Webhook URL

Webhook URL must be provided with each SMS request using the `receiptURL` parameter:

```bash theme={null}
curl -X POST "https://api.loyalty.lt/lt/sms/send" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -H "X-API-Secret: your_api_secret" \
  -d '{
    "source": "MyBrand",
    "destination": "+37061234567",
    "message": "Your verification code: 123456",
    "receipt": true,
    "receiptURL": "https://your-site.com/webhooks/sms"
  }'
```

<Warning>
  You must include `receiptURL` in every request where you want to receive delivery status updates. There is no default webhook URL setting.
</Warning>

## Webhook Payload

When a message status changes, we send a POST request with the following JSON payload:

```json theme={null}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "externalId": "your-reference-123",
  "status": "DELIVERED",
  "recipient": "+37061234567",
  "timestamp": "2025-01-15T10:30:05.000Z",
  "error": null
}
```

## Payload Fields

| Field        | Type   | Description                                                    |
| ------------ | ------ | -------------------------------------------------------------- |
| `id`         | string | Internal message UUID                                          |
| `externalId` | string | Your reference ID from your system (if provided when sending)  |
| `status`     | string | Current status: `SENT`, `DELIVERED`, `FAILED`, `UNDELIVERABLE` |
| `recipient`  | string | Recipient phone number                                         |
| `timestamp`  | string | ISO 8601 timestamp when status changed                         |
| `error`      | string | Error details if failed (null otherwise)                       |

## Status Values

| Status          | Description                  | When Triggered                               |
| --------------- | ---------------------------- | -------------------------------------------- |
| `SENT`          | Message sent to carrier      | Immediately after SMPP submission            |
| `DELIVERED`     | Confirmed delivered to phone | When carrier confirms DLR                    |
| `FAILED`        | Delivery failed              | When carrier rejects message                 |
| `UNDELIVERABLE` | Number unreachable           | When number is invalid, expired, or rejected |

<Note>
  Statuses are uppercase: `SENT`, `DELIVERED`, `FAILED`, `UNDELIVERABLE`
</Note>

## Handling Webhooks

### Example: Node.js / Express

```javascript theme={null}
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/sms', async (req, res) => {
  const { id, externalId, status, recipient, timestamp, error } = req.body;
  
  console.log(`SMS ${id} to ${recipient}: ${status}`);
  
  switch (status) {
    case 'DELIVERED':
      // Mark message as delivered in your database
      await db.messages.update({ 
        where: { externalId: externalId },
        data: { status: 'delivered', deliveredAt: new Date(timestamp) }
      });
      break;
      
    case 'FAILED':
    case 'UNDELIVERABLE':
      // Handle failure - maybe retry or notify admin
      console.error(`SMS failed: ${error}`);
      await db.messages.update({
        where: { externalId: externalId },
        data: { status: 'failed', errorMessage: error }
      });
      break;
      
    case 'SENT':
      // Message sent to carrier, waiting for DLR
      await db.messages.update({
        where: { externalId: externalId },
        data: { status: 'sent', sentAt: new Date(timestamp) }
      });
      break;
  }
  
  // Always respond with 200 to acknowledge receipt
  res.status(200).json({ received: true });
});

app.listen(3000);
```

### Example: PHP

```php theme={null}
<?php
// webhooks/sms.php

$payload = json_decode(file_get_contents('php://input'), true);

$id = $payload['id'];
$externalId = $payload['externalId'];
$status = $payload['status'];
$recipient = $payload['recipient'];
$timestamp = $payload['timestamp'];
$error = $payload['error'];

// Log the webhook
error_log("SMS Webhook: $id to $recipient - $status");

// Update your database
switch ($status) {
    case 'DELIVERED':
        $stmt = $pdo->prepare("UPDATE sms_messages SET status = 'delivered', delivered_at = ? WHERE external_id = ?");
        $stmt->execute([$timestamp, $externalId]);
        break;
        
    case 'SENT':
        $stmt = $pdo->prepare("UPDATE sms_messages SET status = 'sent', sent_at = ? WHERE external_id = ?");
        $stmt->execute([$timestamp, $externalId]);
        break;
        
    case 'FAILED':
    case 'UNDELIVERABLE':
        $stmt = $pdo->prepare("UPDATE sms_messages SET status = 'failed', error_message = ? WHERE external_id = ?");
        $stmt->execute([$error, $externalId]);
        break;
}

// Respond with 200
http_response_code(200);
echo json_encode(['received' => true]);
```

### Example: Python / Flask

```python theme={null}
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks/sms', methods=['POST'])
def sms_webhook():
    payload = request.json
    
    msg_id = payload['id']
    external_id = payload['externalId']
    status = payload['status']
    recipient = payload['recipient']
    timestamp = payload['timestamp']
    error = payload.get('error')
    
    print(f"SMS {msg_id} to {recipient}: {status}")
    
    if status == 'DELIVERED':
        db.execute(
            "UPDATE sms_messages SET status = 'delivered', delivered_at = %s WHERE external_id = %s",
            [timestamp, external_id]
        )
    elif status == 'SENT':
        db.execute(
            "UPDATE sms_messages SET status = 'sent', sent_at = %s WHERE external_id = %s",
            [timestamp, external_id]
        )
    elif status in ['FAILED', 'UNDELIVERABLE']:
        db.execute(
            "UPDATE sms_messages SET status = 'failed', error_message = %s WHERE external_id = %s",
            [error, external_id]
        )
    
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Respond Quickly" icon="bolt">
    Return HTTP 200 within 5 seconds. Process data asynchronously if needed.
  </Card>

  <Card title="Handle Duplicates" icon="copy">
    Webhooks may be sent multiple times. Use `message_id` to deduplicate.
  </Card>

  <Card title="Verify Source" icon="shield">
    Validate webhook comes from Loyalty.lt IP ranges or use signature verification.
  </Card>

  <Card title="Log Everything" icon="file-lines">
    Log all webhook payloads for debugging and audit purposes.
  </Card>
</CardGroup>

## Retry Policy

If your webhook endpoint fails to respond with HTTP 2xx, we retry with increasing delays:

| Attempt     | Delay      |
| ----------- | ---------- |
| 1st retry   | 30 seconds |
| 2nd retry   | 2 minutes  |
| 3rd retry   | 10 minutes |
| 4th retry   | 30 minutes |
| 5th (final) | 1 hour     |

<Warning>
  After 5 failed attempts (over \~2 hours total), the webhook is abandoned. Make sure your endpoint is reliable and responds quickly.
</Warning>

## Testing Webhooks

### Using ngrok for Local Development

```bash theme={null}
# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# Expose it publicly
ngrok http 3000

# Use the ngrok URL as your receiptURL
# https://abc123.ngrok.io/webhooks/sms
```

### Test Payload

Use this sample payload to test your webhook handler:

```bash theme={null}
curl -X POST "https://your-site.com/webhooks/sms" \
  -H "Content-Type: application/json" \
  -H "User-Agent: Loyalty-LT-SMPP-Gateway/1.0" \
  -d '{
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "externalId": "my-ref-123",
    "status": "DELIVERED",
    "recipient": "+37061234567",
    "timestamp": "2025-01-15T10:30:05.000Z",
    "error": null
  }'
```

<Tip>
  Webhooks from Loyalty.lt include the header `User-Agent: Loyalty-LT-SMPP-Gateway/1.0` which you can use for verification.
</Tip>

## Security Recommendations

### Webhook IP Addresses

All webhooks are sent from the following IP addresses. **Whitelist these in your firewall:**

| IP Address       | Description              |
| ---------------- | ------------------------ |
| `185.170.198.15` | Primary webhook server   |
| `5.189.181.34`   | Secondary webhook server |

```nginx theme={null}
# Nginx example
location /webhooks/sms {
    allow 185.170.198.15;
    allow 5.189.181.34;
    deny all;
    
    # ... your proxy config
}
```

### Additional Security Steps

<Steps>
  <Step title="Use HTTPS">
    Always use HTTPS for your webhook endpoint to encrypt data in transit.
  </Step>

  <Step title="Whitelist IP Addresses">
    Only allow requests from `185.170.198.15` and `5.189.181.34`.
  </Step>

  <Step title="Verify Message Exists">
    Before processing, verify the `id` exists in your system and was sent by you.
  </Step>

  <Step title="Rate Limit">
    Implement rate limiting to protect against potential abuse.
  </Step>
</Steps>

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Send SMS" icon="message" href="/api-reference/endpoints/sms/send">
    Send SMS with webhook configuration
  </Card>

  <Card title="Check Status" icon="magnifying-glass" href="/api-reference/endpoints/sms/status">
    Manually check message status
  </Card>
</CardGroup>
