TVH Voice docs

#Recipe: Numbers and regulatory documents

Ordering is asynchronous at most carriers. The order status follows the carrier: pending → (requirements_needed → in_review →) completed | failed. Regulated numbers (for example Dutch local numbers) need a requirement group: one item per requirement key, documents uploaded first. Submit it; the carrier reviews it (number.requirements.updated). A rejected group shows per-item reasons: correct the items with PATCH and submit again.

Order first and attach the group later (POST /v1/number-orders/{id}/requirement-group), or create the group first and pass requirement_group_id when ordering — carriers with synchronous ordering (capabilities.ordering_is_async == false) need the group up front.

#TypeScript (SDK)

// Buy a number: search → (regulatory requirements) → order → wait for "completed".
import { createVoiceClient } from '@voice/sdk';

const voice = createVoiceClient({ baseUrl: process.env.VOICE_API_URL!, apiKey: process.env.VOICE_API_KEY! });
const carrierId = process.env.CARRIER_ID!;

// Hide this feature in your UI when the carrier cannot do it.
const { data: carrier } = await voice.GET('/v1/carriers/{id}', { params: { path: { id: carrierId } } });
if (!carrier!.capabilities.supports_ordering) throw new Error('This carrier has no number API: add numbers with POST /v1/numbers');

const { data: found } = await voice.GET('/v1/available-numbers', { params: { query: { carrier_id: carrierId, country: 'NL', number_type: 'local', limit: 1 } } });
const pick = found!.data[0]!;

let groupId: string | null = null;
if (pick.requires_requirement_group) {
  // Regulatory information: one item per requirement key; documents are uploaded first.
  const { data: reqs } = await voice.GET('/v1/regulatory-requirements', { params: { query: { carrier_id: carrierId, country: 'NL', number_type: 'local' } } });
  const { data: doc } = await voice.POST('/v1/documents', {
    body: { filename: 'kvk.pdf', content_type: 'application/pdf', content_base64: Buffer.from('%PDF-1.4 example').toString('base64') },
  });
  const items = reqs!.data.map((r) => (r.kind === 'document' ? { key: r.key, document_id: doc!.id } : { key: r.key, value: 'Acme B.V., Damrak 1, 1012 LG Amsterdam' }));
  const { data: group } = await voice.POST('/v1/requirement-groups', { body: { carrier_id: carrierId, country: 'NL', number_type: 'local', items } });
  await voice.POST('/v1/requirement-groups/{id}/submit', { params: { path: { id: group!.id } } });
  groupId = group!.id;
}

const { data: order, error } = await voice.POST('/v1/number-orders', {
  body: { carrier_id: carrierId, numbers: [pick.e164], requirement_group_id: groupId, label: 'Docs example' },
});
if (error) throw new Error(error.error.message);
// Status follows the carrier (number.order.updated events). Poll here for simplicity.
console.log('order', order.id, order.status, pick.e164);
console.log('ORDER_ID=' + order.id);

#Notes

This example is executed against the sandbox in CI (tests/e2e/specs/m6-docs.spec.ts).