Skip to main content

Inbound routing

Inbound routing answers two questions in order: which tenant owns this call, and which destination inside that tenant.

Which tenant

Matched in this order, first match wins:

  1. an InboundOverride document;
  2. the owning tenant of the trunk, if the trunk names one;
  3. a tenant's exact DID claim (dids.numbers);
  4. a tenant's DID pattern (dids.patterns);
  5. a catch-all InboundOverride (did: "*").

If nothing matches, the call is rejected.

kind: Tenant
metadata: { name: acme }
spec:
domains: [acme.pbx.example.com]
dids:
numbers: ["+31201234567"]
patterns: ['^\+312012345\d{2}$']

A number claimed by two tenants is a validation error, so the ordering between steps 3 and 4 only matters when an exact number falls inside another tenant's pattern — the exact claim wins.

Which destination

Once the tenant is known, its own InboundRoute documents decide where the call goes:

configVersion: lyno/v1
kind: InboundRoute
metadata:
name: main-line # a label; it also breaks ties between equal priorities
tenant: acme
spec:
priority: 10
did: "+31201234567"
destination:
type: extension
target: "200"
---
configVersion: lyno/v1
kind: InboundRoute
metadata:
name: sales-line
tenant: acme
spec:
priority: 20
did: "+31201234568"
destination:
type: ring_group
target: sales
FieldValues
priorityAscending. A catch-all belongs behind the specific numbers.
didThe number as it arrives, or * for a catch-all inside the tenant.
destination.typeUse extension or ring_group — see below.
destination.targetThe extension number, or the ring group name.
recordOptional. Overrides the recording mode for this number.

Order is what makes a catch-all usable: it has to be able to sit behind the exact numbers, and priority is where that lives rather than in the order the documents happen to be read.

Destination types

The schema defines seven destination types, and validation checks all of them — including that the queue, menu or mailbox you named actually exists.

TypeDelivered
extensionyes
ring_groupyes
queueno
ivrno
voicemailno
externalno
hangupno
Only extension and ring_group reach the caller

The other five pass validation and then, at run time, log invalid destination type and answer the call with 404. A DID pointed at a queue does not ring; it fails.

Point the number at an extension whose plan starts with the step you wanted:

kind: InboundRoute
metadata: { name: support-line, tenant: acme }
spec:
priority: 10
did: "+31201234567"
destination: {type: extension, target: "300"} # extension 300 starts with a queue step

That is better configuration anyway — the number picks up time conditions and a fallback along the way.

Routes are matched in priority order, ascending. A tenant with a single number usually needs one route; a tenant with a block adds a * catch-all on a higher number so an unmapped number still lands somewhere:

kind: InboundRoute
metadata: { name: main-line, tenant: acme }
spec:
priority: 10
did: "+31201234567"
destination: {type: extension, target: "200"}
---
kind: InboundRoute
metadata: { name: catch-all, tenant: acme }
spec:
priority: 100
did: "*"
destination: {type: extension, target: "999"} # everything else

Global overrides

An InboundOverride wins over everything a tenant claims. This is how a number is redirected without touching the tenant that owns it:

# Hand acme's number to globex's extension 900.
configVersion: lyno/v1
kind: InboundOverride
metadata:
name: acme-main-to-globex
spec:
priority: 10
did: "+31201234567"
tenant: globex
destination: {type: extension, target: "900"}
---
# Only reassign the tenant; globex's own inbound routes then decide.
configVersion: lyno/v1
kind: InboundOverride
metadata:
name: reassign-234599
spec:
priority: 20
did: "+31201234599"
tenant: globex
---
# Catch-all for numbers no tenant claims.
configVersion: lyno/v1
kind: InboundOverride
metadata:
name: unclaimed
spec:
priority: 100
did: "*"
tenant: acme
destination: {type: extension, target: "999"}
FieldRequiredMeaning
prioritynoAscending; ties break on metadata.name. A catch-all belongs last.
didyesThe number, or * as a global catch-all.
tenantyesThe tenant that will handle the call.
destinationnoPins the destination. Without it, the tenant's own inbound routes run.

An override's destination is delivered by the same code as a tenant's, so the same two types are the ones that work.

An override is the right tool for temporary redirects — porting a number, covering an outage, moving a customer — because it is one document you can add and delete, leaving both tenants' configuration intact.

Worked example

Two tenants, one shared carrier:

configVersion: lyno/v1
kind: Trunk
metadata: { name: carrier }
spec:
host: sip.provider.example
---
configVersion: lyno/v1
kind: InboundOverride
metadata: { name: acme-main-to-globex }
spec:
priority: 10
did: "+31201234567"
tenant: globex
destination: {type: extension, target: "900"}
---
configVersion: lyno/v1
kind: Tenant
metadata: { name: acme }
spec:
domains: [acme.pbx.example.com]
dids:
numbers: ["+31201234567", "+31201234568"]
---
configVersion: lyno/v1
kind: InboundRoute
metadata: { name: catch-all, tenant: acme }
spec:
priority: 100 # behind anything more specific
did: "*"
destination: {type: extension, target: "200"}

A call to +31201234568 reaches acme extension 200 through the catch-all. A call to +31201234567 — which acme also claims — is taken by the override and lands on globex extension 900 instead. Nothing in acme's own documents changed.