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]
)
endTrack 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
endSecure 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.
Related
- Sender Profiles explains inheritance, sharing, and isolation
- Creating a Sender Profile walks through the dashboard wizard
- Create and activate sub-account profiles via the API automates tenant onboarding
- Multi-Tenant Architectures compares shared and per-tenant setups
- Refer to the messages API reference for the full list of send options
Creating a Sender Profile
Create a Sender Profile in the Sent dashboard: identity, destination countries, sender number, WhatsApp, sharing, billing, and TCR compliance settings.
Batch Operations
How to send messages in bulk with the Sent API: batches of up to 1000 recipients, rate limit management, idempotent retries, and bulk contact imports.