Why Wrap at All
The instinct to call an API directly from application code is reasonable for a handful of calls. It stops being reasonable once several parts of a system need translation, once the operational concerns get real, or once anyone asks how the code will be tested.
A thin internal client — a wrapper around whatever the vendor provides — earns its place by owning things that do not belong scattered through application code:
Credential handling and endpoint configuration, in one place rather than in every caller.
Retry policy, decided once with a coherent classification of which errors are worth retrying.
Idempotency, so that duplicate submissions are prevented structurally rather than by each caller remembering.
Observability, so that every request is logged and instrumented consistently.
A domain vocabulary that matches your application rather than the vendor's.
A testing seam, so that application code can be tested without hitting a network.
An isolation boundary, so that a vendor change or a vendor swap touches one module rather than fifty call sites.
That last one is worth being honest about. Wrappers written primarily to enable a future vendor swap tend to become lowest-common-denominator abstractions that make the current vendor worse to use. Write the wrapper to make your application code clear; take the portability as a side effect rather than as the goal.
Model the Job, Not the Endpoint
The most common design mistake is a wrapper that mirrors the HTTP API one method per endpoint. That produces a client which is marginally more convenient than raw requests and expresses nothing about the domain.
The useful abstraction is the job as your application understands it.
Your application probably thinks in terms of "localize this asset into these languages with this configuration, and tell me when each is ready." Whether that is one API call or four, whether results arrive by webhook or polling, and whether the vendor models languages as separate jobs or one batch are implementation details the caller should not carry.
A workable shape:
A submission function taking a source asset reference, a target language list, and a typed configuration object, returning a job handle.
A job handle that can be persisted, reconstructed from storage, and queried for status.
A status representation that is a closed set of states your application understands, not a passthrough of whatever strings the vendor returns.
A result type exposing the artefacts your application needs — subtitle tracks per language, audio tracks per language, the transcript — as typed accessors rather than as a bag of URLs.
An event interface so callers can react to progress without knowing whether it arrived by webhook or by poll.
Keep the vendor's own identifiers on the job handle. When something goes wrong, the first thing support will ask for is their job ID, and a wrapper that has discarded it makes every incident harder.
Type the Configuration
Configuration passed as an untyped map is where integrations rot. A typo in a key name produces a silently ignored setting, and the failure shows up as unexpectedly wrong output weeks later.
Enumerate what you can. Languages, voice selections, output formats, and quality modes are finite sets. Modelling them as enumerated types catches errors at compile time or at construction rather than at the API boundary.
Validate what you cannot enumerate. Reject unknown keys explicitly rather than passing them through — a wrapper that silently forwards unrecognised configuration is not adding safety.
Provide sensible defaults. Most callers should be able to submit a job with a source, a language list, and nothing else.
Make the dangerous options verbose. Anything that overwrites existing work, incurs significant cost, or bypasses review should require an explicit and obviously named parameter. Making a destructive option as easy to set as a harmless one is a design decision, and usually the wrong one.
Separate policy from request. Retry behaviour, timeouts, and concurrency limits are properties of the client instance, not of individual calls. Mixing them into per-request configuration invites callers to set them inconsistently.
Own the Operational Concerns
Retries belong in the client, and only in the client. Nested retry logic across an HTTP library, a service client, and a job runner multiplies request counts and is invisible until it causes throttling. Decide the client owns it, implement exponential backoff with full jitter, honour any Retry-After the service supplies, and document clearly that callers should not add their own.
Classify errors into your own hierarchy. Application code should be able to distinguish, at the type level, between an invalid request, a transient failure, an authentication problem, a quota problem, and an unsupported input. Surfacing a raw HTTP status code forces every caller to reimplement the same classification, and they will do it differently.
Generate idempotency keys internally. Derive them deterministically from the request contents so a retry of the same logical submission carries the same key. Do not make this the caller's responsibility; they will forget.
Instrument everything at the boundary. Request duration, outcome, error class, retry count, and the vendor's job identifier. This is the only place in the system where all of that is visible together.
Apply concurrency limits in the client. A semaphore around outbound requests protects both you and the service, and it means a caller that fans out a thousand submissions does not need to know the safe parallelism.
Redact secrets in logs by construction. A wrapper that logs full request bodies will eventually log a credential. Build the redaction into the logging path rather than relying on care.
Build the Testing Seam First
An integration that can only be tested against the live service will not be tested.
Define the client as an interface with a real implementation. Everything downstream depends on the interface, so a test can substitute a fake.
Ship a fake implementation alongside the real one. Not a mock generated per test — a maintained in-memory implementation that behaves plausibly: jobs progress through states, results appear, errors can be induced on demand. This is worth the effort because it lets application logic be tested exhaustively, including failure paths that are hard to provoke against the real service.
Support recorded interactions for contract tests. Capturing real request-response pairs and replaying them catches schema drift that a hand-written fake will not.
Test the error paths specifically. Timeouts, malformed responses, unexpected status values, truncated payloads. These are where integrations break in production and where they are least often tested.
Run a small live smoke test on a schedule, separately from the main suite. It is the only thing that catches a vendor change before your users do.
Versioning and Change
Version the wrapper independently of the vendor's API. Your callers depend on your interface, and it should be able to remain stable across a vendor point release.
Treat vendor additions as opt-in. A new capability appearing in the API does not need to appear in your interface immediately. Adding it when a caller needs it keeps the surface small.
Handle unknown enum values gracefully. Services add status values and result types. A client that throws on an unrecognised status will break the day the vendor ships a new one. Model an explicit unknown case and let callers decide.
Deprecate with a migration path. When your own interface changes, provide the old shape alongside the new one for a release rather than breaking every caller at once.
Keep a changelog that names behaviour changes, not just version numbers. "Retry policy now honours Retry-After" is the kind of change that alters production behaviour without altering any signature.
What to Leave Out
Wrappers grow, and the ones that stay useful are disciplined about scope.
Do not embed business logic. Which languages to translate into, when to publish, and what quality gates to apply are application decisions. A client that decides them is no longer a client.
Do not cache results silently. Caching is a policy decision with correctness implications, and hiding it inside the client produces surprising staleness. Expose the primitives and let the application decide.
Do not build a workflow engine. Multi-step orchestration belongs above the client. The moment a wrapper grows a state machine for your pipeline, it has stopped being reusable.
Do not wrap what you do not use. Coverage of the vendor's entire API surface is not a goal. Every method you expose is one you maintain.
Do not hide errors. A client that swallows failures and returns empty results produces bugs that take days to find. Fail loudly at the boundary.
A Working Checklist
- Model jobs as your application understands them, not endpoint by endpoint.
- Keep the vendor's job identifier on the handle for support conversations.
- Represent status as a closed set your application controls, with an explicit unknown case.
- Type the configuration and reject unknown keys rather than forwarding them.
- Provide defaults so a simple submission needs three arguments.
- Make destructive or costly options explicitly named and verbose.
- Own retries entirely in the client, with jitter and
Retry-Aftersupport. - Document that callers must not add their own retry layer.
- Classify errors into your own typed hierarchy.
- Generate idempotency keys deterministically inside the client.
- Apply concurrency limiting at the client boundary.
- Instrument duration, outcome, error class, retry count, and vendor job ID.
- Redact secrets in the logging path by construction.
- Define the client as an interface and ship a maintained fake implementation.
- Add contract tests from recorded real interactions.
- Run a scheduled live smoke test separate from the main suite.
- Version the wrapper independently and keep a behaviour-level changelog.
- Keep business logic, caching policy, and orchestration outside the client.
Frequently Asked Questions
Should I write a wrapper if the vendor already ships an SDK?
Often yes, but a thin one. Vendor SDKs are designed for general use and expose the vendor's domain model rather than yours. A thin internal layer over the vendor SDK gives you the testing seam, the error classification, and the domain vocabulary without reimplementing transport. What you should not do is reimplement the vendor SDK's job from scratch.
Will a wrapper let me swap vendors easily?
Partly, and it should not be the main reason to build one. Wrappers designed for portability tend toward lowest-common-denominator abstractions that make the vendor you actually use worse to work with. Build the wrapper to make your application code clear and to own operational concerns; a smaller migration surface is a genuine side benefit, not the goal.
Where should retry logic live?
In the client, and nowhere else. Nested retries across an HTTP library, a service client, and a job runner multiply request counts — three layers of three attempts is twenty-seven requests for one logical call — and the effect is invisible until it causes throttling. Pick the client as the owner, implement it properly there, and document that callers must not add their own.
Is a hand-written fake worth maintaining?
Yes, for anything beyond a couple of call sites. A maintained in-memory implementation where jobs progress through states, results appear, and errors can be induced lets you test application logic exhaustively — including the failure paths that are hard to provoke against a real service and that break in production. Pair it with contract tests from recorded real interactions so the fake does not drift from reality.
How should I handle a status value I do not recognise?
Model it explicitly as an unknown case rather than throwing. Services add status values over time, and a client that rejects anything unrecognised breaks the day the vendor ships one. Log it, surface it as a distinct case, and let the calling code decide whether to wait, ignore, or escalate.
What is the most common thing wrappers get wrong?
Scope creep into orchestration. A client that grows a state machine for your particular pipeline — deciding when to publish, which quality gates to apply, how to sequence languages — has stopped being a client and become a workflow engine that only one application can use. Keep multi-step orchestration above the client.
Related reading: Video Translation API Guide | Error Handling for Video Translation APIs | Webhooks for Video Translation



