Managing Contacts & Templates

This page shows you how to wrap contacts and templates in your service layer. Both follow the same shape as sending messages: a thin service method wraps the SDK resource, maps the snake_case response to your camelCase contract, and lets the controller stay dumb.

Samples on this page are shown in TypeScript, Python, and Go; the patterns are framework-agnostic and transfer directly to the other supported SDK languages.

Concepts first: contacts and templates. Guides: managing contacts and working with templates. This page is about wiring them into your service layer.

Contacts

Three operations: contacts.list, contacts.create, contacts.delete. The mapping is small but non-negotiable: phone_number → phoneNumber, created_at → createdAt, and it happens in the service, never in the controller.

// services/sent.service.ts
async listContacts(): Promise<ContactItem[]> {
  const response = await this.client.contacts.list({ page: 1, page_size: 100 });
  const contacts = response.data?.contacts ?? [];
  return contacts.map((c) => ({
    id: c.id,
    phoneNumber: c.phone_number, // ← snake → camel
    createdAt: c.created_at,
  }));
}

async createContact(phoneNumber: string, sandbox?: boolean): Promise<ContactItem> {
  const response = await this.client.contacts.create({
    phone_number: phoneNumber,
    sandbox: sandbox ?? false,
  });
  const c = response.data ?? {};
  return { id: c.id, phoneNumber: c.phone_number, createdAt: c.created_at };
}

async deleteContact(id: string): Promise<void> {
  await this.client.contacts.delete(id, {});
}

The controller just delegates and picks status codes (201 on create, 204 on delete). validateBody and asyncHandler are the shared helpers from project setup:

// controllers/contacts.controller.ts
router.get('/', asyncHandler(async (req, res) => {
  const { sentService } = servicesForRequest(req);
  res.status(200).json({ items: await sentService.listContacts() });
}));

router.post('/', validateBody(CreateContactSchema), asyncHandler(async (req, res) => {
  const { sentService } = servicesForRequest(req);
  const contact = await sentService.createContact(req.body.phoneNumber, req.body.sandbox);
  res.status(201).json(contact);
}));

router.delete('/:id', asyncHandler(async (req, res) => {
  const { sentService } = servicesForRequest(req);
  await sentService.deleteContact(String(req.params.id));
  res.status(204).send();
}));

Templates

Templates are read-only from your integration's side. You list them and retrieve them; you don't create them here (that's a console/approval workflow). Two operations: templates.list and templates.retrieve.

Two mapping gotchas to handle explicitly:

  • The SDK exposes channels (an array); your contract wants a single channel, so take channels[0].
  • The SDK Template model carries no rendered body, so emit null for it. Don't invent it.
// services/sent.service.ts
async listTemplates(): Promise<TemplateItem[]> {
  const response = await this.client.templates.list({ page: 1, page_size: 100 });
  const templates = response.data?.templates ?? [];
  return templates.map((t) => ({
    id: t.id,
    name: t.name,
    channel: t.channels?.[0],  // ← channels[] → channel
    status: t.status,
    body: null,                // ← SDK has no rendered body — always null
    category: t.category,
    language: t.language,
  }));
}

async getTemplate(id: string): Promise<TemplateItem> {
  const response = await this.client.templates.retrieve(id);
  const t = response.data ?? {};
  return {
    id: t.id, name: t.name, channel: t.channels?.[0], status: t.status,
    body: null, category: t.category, language: t.language,
  };
}

Looking up templates by id or name

The send API accepts a template id or name. You don't need both. Prefer resolving by name and letting templates.list give you the id when you need it, rather than pasting UUIDs around your codebase.

// Resolve a stable, human-readable name to whatever the current template is.
async function resolveTemplate(sent: SentService, name: string): Promise<TemplateItem> {
  const templates = await sent.listTemplates();
  const match = templates.find((t) => t.name === name);
  if (!match) throw new ApiError(404, 'TemplateNotFound', `No template named "${name}"`);
  return match;
}

Don't ship hard-coded template UUIDs scattered through your business logic. In real code, map a small set of stable names (welcome, order_update) to templates, resolve them at the edge, and let the id float. When a template is re-approved with a new id, nothing in your code changes. Docs snippets inline an id (templateId: 'welcome-template-id') only for brevity.

Validate at send time, too: a template must be approved for the channel you're sending on. Check status === "APPROVED" (and that the resolved channel matches your intended channel) before you call send, so you fail with a clear error instead of a cryptic API rejection.

Budget real time for approval before your go-live date. WhatsApp template approval typically takes 24-48 hours (see Create your first template). SMS templates aren't gated the same way, so if you're SMS-only you can usually send immediately. Build and test against a sandbox: true send while waiting; don't let approval latency be a surprise on launch day.

resolveTemplate as shown makes a full listTemplates round trip on every send. Fine for a first pass, but templates change rarely, so a short-TTL cache (30-60s in memory, or your shared store once you're on multiple instances) removes that extra call and its rate-limit cost from your hot send path without meaningfully delaying a template re-approval from taking effect.

Why the wrapper matters

Every method here does the same three things: call one SDK resource, map snake→camel, translate errors. That uniformity is the point.

  • Controllers depend on your service interface, never on SDK types, so the SDK is mockable and swappable in tests.
  • The contract stays stable even when the SDK's field names or shapes drift.
  • The client is still built per request (servicesForRequest) from the bearer key. These reads are just as scoped as the sends. See Authentication.

Next steps

On this page