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 sub-account profiles 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

Webhook events do not carry your tenant identifiers, so attribute them yourself: store the message_id from each send response against the profile that sent it, then look the message up when the event arrives. See the events reference for the full envelope.

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

  if (field === 'message' && payload.message_status === 'SENT') {
    // Retrieve sender profile context from your database
    const message = await db.messages.findBySentId(payload.message_id);

    if (message?.senderProfileId) {
      await analytics.track('message_sent', {
        senderProfileId: message.senderProfileId,
        messageId: payload.message_id,
        channel: payload.channel,
        inboundNumber: payload.inbound_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":
        # Retrieve sender profile context from your database
        message = db.messages.find_by_sent_id(p["message_id"])

        if message and message.sender_profile_id:
            analytics.track("message_sent", {
                "sender_profile_id": message.sender_profile_id,
                "message_id": p["message_id"],
                "channel": p["channel"],
                "inbound_number": p["inbound_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 {
            MessageID     string `json:"message_id"`
            MessageStatus string `json:"message_status"`
            Channel       string `json:"channel"`
            InboundNumber string `json:"inbound_number"`
        } `json:"payload"`
    }
    json.NewDecoder(r.Body).Decode(&event)
    w.WriteHeader(http.StatusOK)

    if event.Field == "message" && event.Payload.MessageStatus == "SENT" {
        // Retrieve sender profile context from your database
        message, _ := db.Messages.FindBySentID(event.Payload.MessageID)

        if message != nil && message.SenderProfileID != "" {
            analytics.Track("message_sent", map[string]interface{}{
                "sender_profile_id": message.SenderProfileID,
                "message_id":        event.Payload.MessageID,
                "channel":           event.Payload.Channel,
                "inbound_number":    event.Payload.InboundNumber,
            })
        }
    }
}
@PostMapping("/webhooks/sent")
public ResponseEntity<Void> webhook(@RequestBody WebhookEvent event) {
    if ("message".equals(event.getField()) && "SENT".equals(event.getPayload().getMessageStatus())) {
        // Retrieve sender profile context from your database
        MessageRecord message = db.messages().findBySentId(event.getPayload().getMessageId());

        if (message != null && message.getSenderProfileId() != null) {
            analytics.track("message_sent", Map.of(
                "sender_profile_id", message.getSenderProfileId(),
                "message_id",        event.getPayload().getMessageId(),
                "channel",           event.getPayload().getChannel(),
                "inbound_number",    event.getPayload().getInboundNumber()
            ));
        }
    }
    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")
        {
            // Retrieve sender profile context from your database
            var message = await db.Messages.FindBySentIdAsync(evt.Payload.MessageId);

            if (message?.SenderProfileId != null)
            {
                await analytics.TrackAsync("message_sent", new {
                    sender_profile_id = message.SenderProfileId,
                    message_id        = evt.Payload.MessageId,
                    channel           = evt.Payload.Channel,
                    inbound_number    = evt.Payload.InboundNumber
                });
            }
        }
        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') {
        // Retrieve sender profile context from your database
        $message = $this->db->messages->findBySentId($p['message_id']);

        if ($message && $message->senderProfileId) {
            $this->analytics->track('message_sent', [
                'sender_profile_id' => $message->senderProfileId,
                'message_id'        => $p['message_id'],
                'channel'           => $p['channel'],
                'inbound_number'    => $p['inbound_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'
    # Retrieve sender profile context from your database
    message = db.messages.find_by_sent_id(p['message_id'])

    if message && message.sender_profile_id
      analytics.track('message_sent', {
        sender_profile_id: message.sender_profile_id,
        message_id:        p['message_id'],
        channel:           p['channel'],
        inbound_number:    p['inbound_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 the delivery events for that message_id resolve to the same profile in your database. If events arrive but the lookup finds no message, you are not persisting the message_id returned by the send call.

On this page