---
title: "Safe to depend on"
url: https://services.dev/docs/sdk/types
---
# Safe to depend on

URL: https://services.dev/docs/sdk/types

> Your brand's client types each Service's names as literal unions, returns an OFFER as a value, narrows every refusal to its operation's codes, and verifies webhooks into a typed event.

A developer building on your brand should learn at compile time what would otherwise break in production. Every type in the generated client comes from your manifest and the registries, so it says exactly what your brand serves, and nothing more.

## Names are literal unions

Each Service's input, output, stages, ends, policy and signals:

```ts
  translation: {
    input: { document: File; to: string }
    output: { document: File }
    stage: "draft" | "review"
    end: "lapsed"
    policy: {}
    signal: never
  }
```

So a typo fails before it runs, in the words tsc printed:

```txt
// a field Translation doesn't take
await lingua.translation.order({ document: lease, to: 'fr-CA', pages: 3 })
  ✗ Object literal may only specify known properties, and 'pages' does not exist in type 'Order<"translation">'.
// an end no Service of lingua's declares
await lingua.jobs.end('job_7Hq2', { end: 'won' })
  ✗ Type '"won"' is not assignable to type '"lapsed"'.
// a stage Translation never shows
if (job.service === 'translation' && job.stage === 'certify') return
  ✗ This comparison appears to be unintentional because the types '"draft" | "review" | null' and '"certify"' have no overlap.
// an id that isn't a job's
await lingua.jobs.get('7Hq2')
  ✗ Argument of type '"7Hq2"' is not assignable to parameter of type '`job_${string}`'.
```

## An OFFER is a value

An order without `max_usd` returns the offer. Nothing throws, and `'type' in offer` tells the two apart:

```ts
  const offer = await lingua.translation.order({ document: lease, to: 'fr-CA', key: 'lease' })
  if ('type' in offer) offer.price.sentence satisfies string

  const ordered = await lingua.translation.order({ document: lease, to: 'fr-CA', max_usd: 49, key: 'lease' })
  if ('type' in ordered) return ordered.next.http
```

```txt
const offer = await lingua.translation.order({ document: lease, to: "fr-CA", key: "lease" })
← OFFER · $49 per job, within 48 hours, charged on delivery.   returned, not thrown
  next    order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"}

const job = await lingua.translation.order(offer.next.arguments)
← job_7Hq2 · working · $49.00 held
```

A job that isn't there is a `not_found` refusal, EMPTY at 404, thrown like any other.

## Each refusal narrows to its operation

A refusal throws a `ServicesError` subclass for its type: `InvalidRequestError`, `BlockedError`, `ConflictError` and the rest. `isServicesError(e, op)` narrows `e.code` to the codes that operation can answer:

```ts
  try {
    await lingua.jobs.accept(job.id)
  } catch (e) {
    if (isServicesError(e, 'job.accept')) e.code satisfies 'not_found' | 'not_delivered' | 'needs_the_party' | 'key_missing' | 'key_invalid' | 'body_invalid' | 'version_unknown' | 'api_error'
    if (e instanceof ConflictError) e.category satisfies 'conflict'
  }
```

```txt
await lingua.jobs.accept("job_7Hq2")   // still working
× throws ConflictError<'job.accept'> · 409 · BLOCKED · not_delivered
  reason  Only a delivered job can be accepted or sent back.
  isServicesError(e, 'job.accept')  true

await lingua.jobs.get("job_none")
× throws InvalidRequestError<'job.get'> · 404 · EMPTY · not_found
  reason  No job job_none.
  isServicesError(e, 'job.get')  true
```

Test for a code the operation never answers, and tsc says so:

```txt
// a code job.accept never answers
if (isServicesError(e, 'job.accept') && e.code === 'ask_expired') return
  ✗ This comparison appears to be unintentional because the types '…' and '"ask_expired"' have no overlap.
```

## Webhooks verify into the union

`webhooks.verify()` checks the `services-signature` header against the watch's `whsec_` secret and its age, then returns the event narrowed on `type`:

```ts
export function receive(body: string, header: string, secret: string) {
  const event = webhooks.verify(body, header, secret)
  if (event.type === 'job.delivered') return event.job
}
```

```txt
export type EventType = "job.ordered" | "step.committed" | "step.skipped" | "job.paused" | "job.delivered" | "job.sent_back" | "job.accepted" | "job.canceled" | "job.ended" | "job.at_risk" | "job.late" | "policy.set" | "signal.raised" | "outcome.reported" | "charge.held" | "charge.collected" | "charge.earned" | "charge.released" | "charge.voided" | "charge.refunded" | "submission.sent" | "receipt.accepted" | "receipt.rejected" | "receipt.timed_out" | "ask.opened" | "item.done" | "ask.reminded" | "ask.done" | "ask.expired" | "ask.voided" | "ask.relinked" | "grant.given" | "grant.expired" | "grant.revoked" | "thread.posted" | "message.sent" | "consent.given" | "clock.opened" | "clock.noticed" | "clock.escalated" | "clock.ack_due" | "clock.ack_missed" | "clock.ran" | "clock.moved" | "clock.due" | "clock.satisfied" | "clock.rolled" | "clock.recurred" | "clock.acked" | "clock.chosen" | "clock.assigned" | "clock.outlived" | "subscription.created" | "subscription.renewed" | "subscription.minted" | "allowance.used" | "allowance.exceeded" | "subscription.canceled"
export type Event = { [T in EventType]: EventOf<T> }[EventType]

services-signature: t=1792936931,v1=bcd16e28974679fcbeb5e926812867dd1029ffd7caa115baeff4e4d0504dcd48
const event = webhooks.verify(body, header, secret)
← job.ordered · evt_5gX1X2B5swzY61 · job_7Hq2

// the body changed
× throws SignatureVerificationError: The signature doesn't match: wrong secret, or the body changed.
// ten minutes late
× throws SignatureVerificationError: The signature is 600 seconds old, past the 300-second tolerance.
```

Watching an event a buyer never receives is a type error too:

```txt
// an event a watch never receives
await lingua.watches.create({ url: 'https://lingua.example/hook', events: ['step.proposed'] })
  ✗ Type '"step.proposed"' is not assignable to type 'EventType'.
```

## When the types change

A new stage, end, event or error code is additive on the wire, but it breaks a `switch` that was exhaustive over the old union. `diff` marks every such change, so you see it before your buyers' builds do. `connect({ version })` pins the API version a client was written against.

---

- Up: [The SDKs](https://services.dev/docs/sdk.md)
- Previous: [The SDKs](https://services.dev/docs/sdk.md)
- Next: [A buyer that is software](https://services.dev/docs/sdk/agent.md)
