# Build Your App Function

In this article, you will configure your App Function with the Twilio credentials, understand the payload your function receives, and write the function that delivers WhatsApp messages through Twilio.

By the end of this article, you will have a working App Function that receives a communication event from Salla and sends a WhatsApp message to the customer.

<!--

## Configure App Settings

App Settings is where you store your provider credentials. Your App Function reads them at runtime via `context.settings` — this keeps secrets out of your code and lets each merchant supply their own credentials when they install your app.

1. In the Partner Portal, open your app and navigate to **App Settings**.
2. Add the following fields:

<!-- IMAGE: Partner Portal — App Settings form with fields for SID, token, and phone number -->

<!--

| Field name | Value | Maps to in code |
|---|---|---|
| `twilio_account_sid` | Your Account SID from Step 2 | `context.settings.twilio_account_sid` |
| `twilio_auth_token` | Your Auth Token from Step 2 | `context.settings.twilio_auth_token` |
| `twilio_phone_number` | Your Twilio phone number from Step 2 | `context.settings.twilio_phone_number` |

3. Save the settings form.

> **Fill this in before testing:** If `context.settings` is empty when you run the preview panel in Step 4, it means you have not yet filled in the App Settings form on your demo store. Go to **Apps → Settings → Customize** on your demo store dashboard and enter the values there.

-->

## Add a new App Function

<Steps>
<Step>

In the Partner Portal, open your app and scroll to **App Functions**.
</Step>
<Step>
2. Click **Add New Function**.


![image.png](https://api.apidog.com/api/v1/projects/451700/resources/375475/image-preview)

:::note[]
The function builder consists of four main sections:
- Function Name: A descriptive identifier 
- Action Selector: The specific event that triggers this function.
- Code Editor: Where you write the custom handler logic.
- Preview Panel: A testing environment using real demo store data.


![image.png](https://api.apidog.com/api/v1/projects/451700/resources/375476/image-preview)
    
:::

</Step>
    
<Step>
Click Select Action and choose the channel you want to handle. The code editor will be auto-populated with boilerplate code which we will replace using the code in this article in the following sections.


![image.png](https://api.apidog.com/api/v1/projects/451700/resources/375478/image-preview)




</Step>
</Steps>
## Understand the payload

Before writing your handler, it helps to know exactly what Salla sends to your function. Every communication event delivers the same shape inside `context.payload.data`.

| Field | Type | Description |
|---|---|---|
| `notifiable` | `string[]` | One or more recipients — phone numbers for SMS/WhatsApp, email addresses for Email |
| `type` | `string` | Why this message is being sent (e.g. `order.status.updated`, `auth.otp.verification`) |
| `content` | `string` | The ready-to-send message body — use this as-is or reformat it for your provider |
| `entity` | `object \| null` | The related store entity (order, shipment, product, etc.) — can be `null`, always check before accessing |
| `meta` | `object` | Additional context such as `customer_id` or OTP `code` |

Here is what a real `communication.whatsapp.send` payload looks like when an order status changes:

```json
{
  "event": "communication.whatsapp.send",
  "data": {
    "notifiable": ["+966500000000"],
    "type": "order.status.updated",
    "content": "Your order #1234 status has been updated to: Shipped.",
    "entity": {
      "id": 1234,
      "type": "order"
    },
    "meta": {
      "customer_id": 987
    }
  }
}
```

:::warning[Always guard `entity` and `meta`]
Both fields can be `null` or missing depending on the event type. Accessing them without a null check is the most common cause of a 500 error. Use optional chaining (`entity?.id`) in your handler.
:::


## Write your handler

In this section, there are two tabs:

- **Explain Code** — walks through the handler section by section with inline annotations
- **Full Code** — the complete handler ready to copy and paste

Follow whatever fits you

<Tabs>
  <Tab title="Code Explanation">

<Steps>
  <Step title="Set Your Twilio Credentials">
    These are the credentials from your Twilio Console. Replace the placeholder values with your actual Account SID, Auth Token, and phone number. In production, these should come using the [App Settings](https://salla.dev/blog/how-to-build-app-settings-form/) feature instead of being hardcoded.

```typescript
    const accountSid = "YOUR_TWILLO_ACCOUNTSID";
    const authToken = "YOUR_TWILLO_AUTHTOKEN";
    const fromNumber = 'whatsapp:+1415xxxx';
    const customerPhone = '+9665xxxx';
```

:::note[Hardcoded vs. App Settings]
    Hardcoding credentials is fine for local testing but should not go to production. Move these values to [App Settings](https://salla.dev/blog/how-to-build-app-settings-form/) and read them via `context.settings.twilio_account_sid` etc. before publishing.
:::
  </Step>

  <Step title="Validate the Recipient">
    Before sending anything, confirm the target customer phone number is actually in the `notifiable` array Salla sent. If it is not, log it and return a success early — this is not an error, just a message that was not meant for this customer.

```typescript
    if (!data.notifiable?.includes(customerPhone)) {
      console.log("Customer not in notifiable list:", data.notifiable);
      return Resp.success();
    }
```
  </Step>

  <Step title="Extract the Order ID">
    Pull the order ID from `entity`. This can be `null` depending on the event, so always guard it. If there is no order entity, log it and return early.

```typescript
    const orderId = data.entity?.id;
    if (!orderId) {
      console.log("No order entity in payload:", data);
      return Resp.success();
    }
```
  </Step>

  <Step title="Build the Message">
    Compose the WhatsApp message text using the order ID and `data.content`. The `data.content` field is the ready-to-send body Salla prepared — use it as a fallback or as the primary message depending on your needs. Format the recipient number with the `whatsapp:` prefix that Twilio requires.

```typescript
    const messageText = `Hi! Your order #${orderId} is now ${data.content || "updated"}.`;
    const toNumber = `whatsapp:${customerPhone}`;

    const body = new URLSearchParams();
    body.append('From', fromNumber);
    body.append('To', toNumber);
    body.append('Body', messageText);
```
  </Step>

  <Step title="Call the Twilio API">
    Send the message using Twilio's Messages API. The request uses HTTP Basic Auth with your Account SID and Auth Token encoded in Base64. The full URL includes your Account SID.

```typescript
    const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`;

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: 'Basic ' + btoa(`${accountSid}:${authToken}`),
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: body.toString(),
    });
```
  </Step>

  <Step title="Handle the Response">
    Parse Twilio's response. If the request succeeded, return the Twilio message SID as confirmation. If Twilio returned an error, surface it with the original status code so Salla can log it correctly.

```typescript
    const result = await response.json();
    console.log("Twilio response:", result);

    if (!response.ok) {
      return Resp.error()
        .setMessage('Twilio error: ' + JSON.stringify(result))
        .setStatus(response.status);
    }

    return Resp.success()
      .setMessage('WhatsApp message sent successfully via Twilio.')
      .setData({ message_id: result.sid });
```
  </Step>

  <Step title="Catch Unexpected Errors">
    Wrap the API call in a try/catch to handle network failures or unexpected exceptions. Return a `500` with the error message so it appears in the function logs.

```typescript
    } catch (err: any) {
      console.error("Error sending WhatsApp:", err);
      return Resp.error()
        .setMessage(err.message || 'Unknown error')
        .setStatus(500);
    }
```
  </Step>

  <Step title="Unhandled Event Fallback">
    If the event type does not match any of your handlers, return a success. This is intentional — an unrecognised event is not an error, it just means your app has nothing to do for it.

```typescript
    return Resp.success();
```
  </Step>
</Steps>
      
  </Tab>
  <Tab title="Full Code">
```
export default async (context: CommunicationEvent): Promise<Resp> => {

  const { payload, settings } = context;
  const data = payload.data;

  // DEBUG — remove after fixing
  console.log("RAW EVENT:", JSON.stringify({ type: data.type, notifiable: data.notifiable, entity: data.entity }, null, 2));

  // -----------------------------
  // 1️⃣ ORDER STATUS UPDATED
  // -----------------------------
  
  if (data.type === "order.status.updated") {
    const accountSid = "YOUR_TWILLO_ACCOUNTSID";
    const authToken = "YOUR_TWILLO_AUTHTOKEN";
    const fromNumber = 'whatsapp:+1415xxxx'; // add the Twillo number
    const customerPhone = '+9665xxxx';

    if (!data.notifiable?.includes(customerPhone)) {
      console.log("Customer not in notifiable list:", data.notifiable);
      return Resp.success();
    }

    const orderId = data.entity?.id;
    if (!orderId) {
      console.log("No order entity in payload:", data);
      return Resp.success();
    }

    const messageText = `Hi! Your order #${orderId} is now ${data.content || "updated"}.`;
    const toNumber = `whatsapp:${customerPhone}`;

    const body = new URLSearchParams();
    body.append('From', fromNumber);
    body.append('To', toNumber);
    body.append('Body', messageText);

    const url = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`;

    try {
      console.log("Sending WhatsApp:", messageText);

      const response = await fetch(url, {
        method: 'POST',
        headers: {
          Authorization: 'Basic ' + btoa(`${accountSid}:${authToken}`),
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: body.toString(),
      });

      const result = await response.json();
      console.log("Twilio response:", result);

      if (!response.ok) {
        return Resp.error()
          .setMessage('Twilio error: ' + JSON.stringify(result))
          .setStatus(response.status);
      }

      return Resp.success()
        .setMessage('WhatsApp message sent successfully via Twilio.')
        .setData({ message_id: result.sid });

    } catch (err: any) {
      console.error("Error sending WhatsApp:", err);
      return Resp.error()
        .setMessage(err.message || 'Unknown error')
        .setStatus(500);
    }
  }

  // -----------------------------
  // Event not handled
  // -----------------------------
  return Resp.success();

  }; 
```

  </Tab>
</Tabs>

## What you have now

- An App Function created and bound to `communication.whatsapp.send`
- A handler that validates credentials, extracts the payload, calls Twilio, and returns a structured response

In the next article, you will test this function end-to-end by creating a real order on your demo store and confirming the WhatsApp message arrives on your phone.

