Integrating Sender Profiles into Your Application

This guide shows you how to route messaging traffic through Sender Profiles in your own app: send with a profile's API key, map customers to profiles in a multi-tenant platform, and attribute webhook events to the right profile. It assumes your organization already has at least one profile (create one in the dashboard or via the API) and that you can already send messages. For the inheritance model behind profiles, see Sender Profiles.

Send with a profile's API key

Each Sender Profile has its own API credentials. The API key you authenticate with determines which profile's resources (templates, contacts, numbers, and compliance settings) the request uses, so sending as a profile is just sending with that profile's key:

curl -X POST "https://api.sent.dm/v3/messages" \
  -H "x-api-key: $PROFILE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["+1234567890"],
    "template": {"id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"}
  }'
import SentDm from '@sentdm/sentdm';

// Uses profile-specific API key
const client = new SentDm({ apiKey: process.env.PROFILE_API_KEY });

await client.messages.send({
  to: ['+1234567890'],
  template: { id: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' }
});
from sent_dm import SentDm

client = SentDm(api_key=os.environ['PROFILE_API_KEY'])

client.messages.send(
    to=["+1234567890"],
    template={"id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8"}
)
package main

import (
    "context"
    "os"
    "github.com/sentdm/sent-dm-go"
    "github.com/sentdm/sent-dm-go/option"
)

func main() {
    client := sentdm.NewClient(
        option.WithAPIKey(os.Getenv("PROFILE_API_KEY")),
    )

    client.Messages.Send(context.Background(), sentdm.MessageSendParams{
        To: []string{"+1234567890"},
        Template: sentdm.MessageSendParamsTemplate{
            ID: sentdm.String("7ba7b820-9dad-11d1-80b4-00c04fd430c8"),
        },
    })
}
SentDmClient client = SentDmOkHttpClient.builder()
    .apiKey(System.getenv("PROFILE_API_KEY"))
    .build();

client.messages().send(MessageSendParams.builder()
    .addTo("+1234567890")
    .template(MessageSendParams.Template.builder()
        .id("7ba7b820-9dad-11d1-80b4-00c04fd430c8")
        .build())
    .build());
SentDmClient client = new(apiKey: Environment.GetEnvironmentVariable("PROFILE_API_KEY"));

await client.Messages.Send(new MessageSendParams
{
    To = new List<string> { "+1234567890" },
    Template = new MessageSendParamsTemplate { Id = "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }
});
use SentDM\Client;

$client = new Client($_ENV['PROFILE_API_KEY']);

$client->messages->send(
    to: ['+1234567890'],
    template: ['id' => '7ba7b820-9dad-11d1-80b4-00c04fd430c8']
);
require "sentdm"

client = Sentdm::Client.new(api_key: ENV["PROFILE_API_KEY"])

client.messages.send_(
  to: ["+1234567890"],
  template: { id: "7ba7b820-9dad-11d1-80b4-00c04fd430c8" }
)

If you prefer to keep a single credential instead of one key per profile, send with your organization API key and scope each request with the x-profile-id header (a profile UUID belonging to your organization). Only organization keys can use this header; profile-scoped keys are rejected with 403. The Sender Profiles API guide covers this pattern in detail.

Map each customer to a profile

For platforms serving multiple customers, store the profile-to-customer mapping in your own database and resolve the credentials per request:

async function sendOnBehalfOfCustomer(customerId: string, message: MessageRequest) {
  const profile = await db.senderProfiles.findByCustomer(customerId);
  const client = new SentDm({ apiKey: profile.apiKey });

  return client.messages.send({
    to: message.recipients,
    template: { id: message.templateId },
    variables: message.variables
  });
}
def send_on_behalf_of_customer(customer_id: str, message: dict):
    profile = db.sender_profiles.find_by_customer(customer_id)
    client = SentDm(api_key=profile.api_key)

    return client.messages.send(
        to=message["recipients"],
        template={"id": message["template_id"]},
        variables=message["variables"]
    )
func sendOnBehalfOfCustomer(customerID string, msg MessageRequest) (*sentdm.MessageResponse, error) {
    profile, _ := db.SenderProfiles.FindByCustomer(customerID)
    client := sentdm.NewClient(option.WithAPIKey(profile.APIKey))

    return client.Messages.Send(context.Background(), sentdm.MessageSendParams{
        To:       msg.Recipients,
        Template: sentdm.MessageSendParamsTemplate{ID: &msg.TemplateID},
    })
}
public MessageResponse sendOnBehalfOfCustomer(String customerId, MessageRequest message) {
    SenderProfile profile = db.senderProfiles().findByCustomer(customerId);
    SentDmClient client = SentDmOkHttpClient.builder()
        .apiKey(profile.getApiKey())
        .build();

    return client.messages().send(MessageSendParams.builder()
        .addTo(message.getRecipients().get(0))
        .template(MessageSendParams.Template.builder()
            .id(message.getTemplateId())
            .build())
        .build());
}
public async Task<MessageResponse> SendOnBehalfOfCustomer(string customerId, MessageRequest message)
{
    var profile = await db.SenderProfiles.FindByCustomerAsync(customerId);
    var client = new SentDmClient(apiKey: profile.ApiKey);

    return await client.Messages.Send(new MessageSendParams
    {
        To = message.Recipients,
        Template = new MessageSendParamsTemplate { Id = message.TemplateId },
        Variables = message.Variables
    });
}
function sendOnBehalfOfCustomer(string $customerId, array $message): array
{
    $profile = $this->db->senderProfiles->findByCustomer($customerId);
    $client = new Client($profile->apiKey);

    return $client->messages->send(
        to: $message['recipients'],
        template: ['id' => $message['template_id']],
        variables: $message['variables'] ?? []
    );
}
def send_on_behalf_of_customer(customer_id, message)
  profile = db.sender_profiles.find_by_customer(customer_id)
  client = Sentdm::Client.new(api_key: profile.api_key)

  client.messages.send_(
    to: message[:recipients],
    template: { id: message[:template_id] },
    variables: message[:variables]
  )
end

Track usage per profile in webhooks

Every event names its account in payload.account_id: the Sender Profile for events its clones deliver, and your organization for your own events. Map that ID to your tenant, rather than storing each message_id and looking the message up when its event arrives. The samples skip your organization's own sends by comparing with its ID, ORGANIZATION_ID.

Receive every profile's events at one endpoint by selecting them under sender_profile on your organization's webhook. Sent clones the webhook onto each profile, including profiles you create later, so onboarding a tenant needs no webhook change. See Sender Profile Events for the request.

app.post('/webhooks/sent', async (req, res) => {
  res.sendStatus(200);
  const { field, payload } = req.body;

  if (field === 'message' && payload.message_status === 'SENT') {
    // Your organization's own sends carry its ID
    const senderProfileId = payload.account_id;

    if (senderProfileId !== ORGANIZATION_ID) {
      await analytics.track('message_sent', {
        senderProfileId,
        messageId: payload.message_id,
        channel: payload.channel,
        outboundNumber: payload.outbound_number,
        timestamp: new Date()
      });
    }
  }
});
@app.post("/webhooks/sent")
async def webhook(request: Request):
    data = await request.json()
    p = data.get("payload", {})

    if data.get("field") == "message" and p.get("message_status") == "SENT":
        # Your organization's own sends carry its ID
        sender_profile_id = p["account_id"]

        if sender_profile_id != ORGANIZATION_ID:
            analytics.track("message_sent", {
                "sender_profile_id": sender_profile_id,
                "message_id": p["message_id"],
                "channel": p["channel"],
                "outbound_number": p["outbound_number"],
                "timestamp": datetime.now().isoformat()
            })

    return {"status": "ok"}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    var event struct {
        Field   string `json:"field"`
        Payload struct {
            AccountID      string `json:"account_id"`
            MessageID      string `json:"message_id"`
            MessageStatus  string `json:"message_status"`
            Channel        string `json:"channel"`
            OutboundNumber string `json:"outbound_number"`
        } `json:"payload"`
    }
    json.NewDecoder(r.Body).Decode(&event)
    w.WriteHeader(http.StatusOK)

    if event.Field == "message" && event.Payload.MessageStatus == "SENT" {
        // Your organization's own sends carry its ID
        if event.Payload.AccountID != organizationID {
            analytics.Track("message_sent", map[string]interface{}{
                "sender_profile_id": event.Payload.AccountID,
                "message_id":        event.Payload.MessageID,
                "channel":           event.Payload.Channel,
                "outbound_number":   event.Payload.OutboundNumber,
            })
        }
    }
}
@PostMapping("/webhooks/sent")
public ResponseEntity<Void> webhook(@RequestBody WebhookEvent event) {
    if ("message".equals(event.getField()) && "SENT".equals(event.getPayload().getMessageStatus())) {
        // Your organization's own sends carry its ID
        String senderProfileId = event.getPayload().getAccountId();

        if (!ORGANIZATION_ID.equals(senderProfileId)) {
            analytics.track("message_sent", Map.of(
                "sender_profile_id", senderProfileId,
                "message_id",        event.getPayload().getMessageId(),
                "channel",           event.getPayload().getChannel(),
                "outbound_number",   event.getPayload().getOutboundNumber()
            ));
        }
    }
    return ResponseEntity.ok().build();
}
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
    [HttpPost("sent")]
    public async Task<IActionResult> Webhook([FromBody] WebhookEvent evt)
    {
        if (evt.Field == "message" && evt.Payload.MessageStatus == "SENT")
        {
            // Your organization's own sends carry its ID
            var senderProfileId = evt.Payload.AccountId;

            if (senderProfileId != OrganizationId)
            {
                await analytics.TrackAsync("message_sent", new {
                    sender_profile_id = senderProfileId,
                    message_id        = evt.Payload.MessageId,
                    channel           = evt.Payload.Channel,
                    outbound_number   = evt.Payload.OutboundNumber
                });
            }
        }
        return Ok();
    }
}
#[Post('/webhooks/sent')]
public function webhook(Request $request): JsonResponse
{
    $data = $request->getPayload()->all();
    $p = $data['payload'] ?? [];

    if (($data['field'] ?? '') === 'message' && ($p['message_status'] ?? '') === 'SENT') {
        // Your organization's own sends carry its ID
        $senderProfileId = $p['account_id'] ?? null;

        if ($senderProfileId !== self::ORGANIZATION_ID) {
            $this->analytics->track('message_sent', [
                'sender_profile_id' => $senderProfileId,
                'message_id'        => $p['message_id'],
                'channel'           => $p['channel'],
                'outbound_number'   => $p['outbound_number'],
            ]);
        }
    }

    return new JsonResponse(['status' => 'ok']);
}
post '/webhooks/sent' do
  request.body.rewind
  data = JSON.parse(request.body.read)
  p = data['payload'] || {}

  if data['field'] == 'message' && p['message_status'] == 'SENT'
    # Your organization's own sends carry its ID
    sender_profile_id = p['account_id']

    if sender_profile_id != ORGANIZATION_ID
      analytics.track('message_sent', {
        sender_profile_id: sender_profile_id,
        message_id:        p['message_id'],
        channel:           p['channel'],
        outbound_number:   p['outbound_number']
      })
    end
  end

  { status: 'ok' }.to_json
end

Secure multi-tenant credentials

  • Store each profile's API key in a separate secrets-management entry so one leaked key exposes one tenant, not all of them.
  • Verify webhook signatures before trusting any event.
  • Apply per-profile rate limiting in your own app so one tenant cannot consume another tenant's throughput.

Verify the integration

Send a test message with one profile's key, then confirm its delivery events carry that profile's ID in payload.account_id. If no events arrive at your endpoint, check that your organization's webhook selects them under sender_profile, and that the profile hasn't deleted its clone.

On this page