Build your integrationTargets

Customize generated packages

Add a public helper, configure package checks, and preserve reviewed changes through regeneration.

Customize generated names and defaults through the Spec and configuration. For a linked repository Delivery, commit package customizations to the Draft so Typeship can preserve and check them during regeneration. Start with the change you need:

I want to…Where to startWhat to know
Change generated names or structureNames and structureNames follow the Spec and language conventions; there is no arbitrary package-layout or template override.
Set authentication, headers, retries, or paginationDefaults and runtime optionsGeneration config sets shared behavior; consumers supply credentials and can override runtime options.
Add custom CLI workflowsAdd a custom CLI commandShip a wrapper with shared authentication, help, and a test you can rerun after regeneration.
Ship a helper to every customerPackage-author codeCommit it to the Draft, export it from the package, and configure the checks it needs.
Combine calls into a complete taskCustom workflowsAuthor prerequisites, polling, recovery, and human handoffs; preserve and test the workflow with its generated package.
Preserve an existing SDK interfaceMigrate an existing SDKCompare the generated interface and maintain an adapter for differences the generator cannot express.
Handle an unsupported requirementUnsupported behaviorUse runtime hooks or a reviewed Draft edit with checks; overlapping generator changes require conflict resolution.

Package-author code

A linked repository Delivery preserves package edits committed to its Draft. The following example adds a public factory to a TypeScript SDK for the fictional Parcel API, tests it, then carries it through a Spec update. The same Draft and check workflow applies to CLI, MCP, Python, and Go packages.

Generate a Draft

You need Node.js 20 or later, npm, curl 7.76 or later, jq, a Typeship API key, and write access to the Spec and destination repositories. Configure a repository Delivery for your TypeScript Target, with the npm package name parcel-client.

For this example, save this Spec as openapi.yaml in your source repository. api.parcel.example is fictional; the test below supplies a response without contacting it.

openapi.yaml
openapi: 3.1.0
info:
  title: Parcel
  version: 1.0.0
servers:
  - url: https://api.parcel.example
security:
  - bearerAuth: []
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
paths:
  /shipments:
    get:
      operationId: listShipments
      tags: [shipments]
      responses:
        '200':
          description: Shipments
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required: [shipment_id]
                  properties:
                    shipment_id:
                      type: string

Commit the Spec and select it as the Project's source. Set your IDs and API key in your shell, then generate. These are Typeship credentials, separate from the fictional API’s test-token below:

export TYPESHIP_PROJECT="prj_..."
export TYPESHIP_TARGET="tgt_..."
export TYPESHIP_TOKEN="<your-Typeship-API-key>"

curl --fail-with-body -sS -X POST \
  "https://typeship.dev/api/v1/projects/$TYPESHIP_PROJECT/generate" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" \
  -H "Content-Type: application/json" \
  --data "$(jq -n --arg target "$TYPESHIP_TARGET" '{target_id: $target}')"
TYPESHIP_DRAFT=$(curl --fail-with-body -sS \
  "https://typeship.dev/api/v1/targets/$TYPESHIP_TARGET" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" | jq -r .draft_id)
curl --fail-with-body -sS \
  "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN"

Replace prj_... and tgt_... with your Project and Target IDs. Open the returned pull_request.url, check out its Draft branch in the destination repository, and enter the Target's package directory. All edits and npm commands below run there.

Add the public helper

Append this function to the existing src/index.ts, after the generated declarations. ParcelClient and ClientOptions are declared in that file, so no import or separate re-export is needed. The existing package export points to its compiled dist/index.js.

Append to src/index.ts
export function createParcelClient(
  token: string,
  options: Pick<ClientOptions, "baseUrl" | "fetch"> = {},
): ParcelClient {
  return new ParcelClient({
    ...options,
    bearerToken: token,
    defaultHeaders: { "Request-Source": "parcel-tools" },
  });
}

Save this test in tests/parcel-helper.test.mjs. It imports through the public package entrypoint and checks the outgoing credential and header:

tests/parcel-helper.test.mjs
import assert from "node:assert/strict";
import { test } from "node:test";
import { createParcelClient } from "parcel-client";

test("the public helper sends Parcel credentials and headers", async () => {
  let requests = 0;
  const client = createParcelClient("test-token", {
    fetch: async (url, init) => {
      requests++;
      assert.equal(String(url), "https://api.parcel.example/shipments");
      const headers = new Headers(init.headers);
      assert.equal(headers.get("Authorization"), "Bearer test-token");
      assert.equal(headers.get("Request-Source"), "parcel-tools");
      return Response.json([{ shipment_id: "shp_123" }]);
    },
  });
  const result = await client.shipments.list();
  assert.deepEqual(result, [{ shipment_id: "shp_123" }]);
  assert.equal(requests, 1);
});

Add the script without replacing existing package scripts, then build and run it:

npm pkg set 'scripts.test:helper=node --test tests/parcel-helper.test.mjs'
npm install
npm run build
npm run test:helper

Expect one passing test. A missing export means the helper was not added to src/index.ts or the package was not rebuilt. A header assertion failure means the helper or Spec's bearer scheme differs from the example. Commit src/index.ts, tests/parcel-helper.test.mjs, and the manifest changes to the Draft.

Configure package checks

Every Draft requires generated_surface to pass. This checks that the complete package still exposes the new output's methods, CLI commands, or MCP tools, even when you keep your edits to conflicted files. Restore any missing operations and push the correction to the Draft before releasing. Typeship reruns the checks. This check does not replace tests for your custom behavior.

Target checks is a sibling of config. Updating it replaces the entire check configuration. Retrieve the current Target first and preserve existing entries:

curl --fail-with-body -sS \
  "https://typeship.dev/api/v1/targets/$TYPESHIP_TARGET" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" > target.json
FieldWhat to configure
generatedAny of build, package, and public_entrypoint; all three are enabled when this field is omitted.
customerUnique {name, command} pairs. Commands run in the Target's package directory. An omitted array becomes empty.
repository_requiredExact names of checks your repository already runs on the Draft head. Naming a check does not create its workflow. An omitted array becomes empty.

For example, a complete configuration could be:

Example check configuration
{
  "generated": ["build", "package", "public_entrypoint"],
  "customer": [{"name":"parcel-helper","command":"npm run test:helper"}],
  "repository_required": ["workspace-contracts"]
}

This illustrates the shape; do not replace your existing gates with it. test:helper must exist in the package, as above. Use workspace-contracts only if that is the exact name of an existing repository check that runs for this Draft. Otherwise substitute your check's name, or omit the addition when no workspace check is needed.

This example retains the retrieved configuration, adds the helper check, and requires that existing workspace check:

jq '.checks
  | .generated //= ["build", "package", "public_entrypoint"]
  | .customer = ((.customer // [] | map(select(.name != "parcel-helper")))
      + [{"name":"parcel-helper","command":"npm run test:helper"}])
  | .repository_required = ((.repository_required // [])
      + ["workspace-contracts"] | unique)' target.json > checks.json
Update Target checks with the API
jq -n --slurpfile checks checks.json '{checks: $checks[0]}' > target-update.json
TYPESHIP_DRAFT=$(curl --fail-with-body -sS -X PATCH \
  "https://typeship.dev/api/v1/targets/$TYPESHIP_TARGET" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" \
  -H "Content-Type: application/json" --data-binary @target-update.json | jq -r .draft_id)
curl --fail-with-body -sS -X POST \
  "https://typeship.dev/api/v1/projects/$TYPESHIP_PROJECT/generate" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" \
  -H "Content-Type: application/json" \
  --data "$(jq -n --arg target "$TYPESHIP_TARGET" '{target_id: $target}')"
curl --fail-with-body -sS \
  "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN"

Check the Draft's status first; Follow the Draft status lists each value and its next step. Then inspect errors, head_sha, and every entry in checks: its name, state, reason, revision, and url. Each required check must be passed for the latest Draft commit, and the Draft status must be ready before merging. pending, failed, and not_assessed are not passes. Follow the failing check's url, fix its command or workflow, push the correction, then retrieve the Draft again. If a check never reports, compare the Target's stored checks with the repository's actual check name and its triggering branches and paths.

The same update is available in CLI builds whose typeship targets update --help lists --checks and whose typeship drafts get --help resolves. Published CLI 0.10.0 lacks these controls; use the HTTP commands above with that version. With a supporting CLI, sign in using typeship login, then run:

typeship targets get tgt_... --json > target.json
# Merge your additions into the retrieved checks as above.
typeship targets update tgt_... --checks "$(cat checks.json)"
typeship projects generate prj_...
typeship drafts get "$(jq -r .draft_id target.json)" --json

Regenerate and verify preservation

In the source Spec, add description: A shipment identifier. beneath shipment_id's type: string, commit it, and repeat the POST to the Project’s /generate endpoint above. Pull the updated Draft in the destination repository. The new model documentation and your factory should both be present.

Run npm run build and npm run test:helper from the package directory again. Retrieve the Draft and confirm the required checks pass on the latest head_sha; earlier green checks do not approve a newer commit. Review the diff, then merge that checked commit.

A helper changes the runtime package and follows the versioned release path. A later change confined to tests or check infrastructure can advance the last merged package without a new version or another publishing run. Publishing explains what the Console shows after each kind of merge.

Custom code during regeneration

Non-overlapping edits survive when Typeship generates again. An overlapping edit requires review: for example, changing a generated model property's documentation on the Draft while changing that same description in the Spec can conflict. Inspect the reported path and combine the intended wording on the Draft before rechecking.

File ownership during regeneration

Ownership collisions, conflicting deletions, binary changes, and mode changes can also require review. See How code is merged for how conflicts are detected, choosing a side, and reusing earlier resolutions.

Follow the Draft status

Each Target names its open Draft in draft_id. GET /drafts/{draft_id} returns a status and, when you need to act, a typed reason. Branch on these fields; each entry in errors adds the specific problem and its suggested_action.

statusWhat it meansNext step
idleThe open Draft has no pending change.No Draft action is pending. You can start a Generation explicitly if automatic generation is off.
workingTypeship is generating, carrying your repository edits forward, applying decisions, or running checks.Retrieve the Draft again.
action_requiredThe typed reason identifies a decision or correction.Follow the reason below.
readyEvery required check passed on head_sha.Merge the pull request.
mergedThe pull request merged, and this Draft is final.Retrieve the Target; its draft_id names the next Draft.
reason when action is requiredNext step
conflictResolve the listed files.
checks_failedEach checks_failed entry in errors names a failed required check. Open its url in checks, correct the package, and push to the Draft.
review_failederrors holds version_too_low or draft_title_invalid. Correct the version or pull request title, and save or push the fix.
checks_unavailableRestore the required check shown in checks.
history_rewrittenReview and approve history recovery.

The Draft response carries an ETag header. To change the version selection only if nobody else changed it since your read, send that value in If-Match with PATCH /drafts/{draft_id}; a mismatch returns 412 precondition_failed without saving.

Inspect and resolve a conflict

When the Draft has status action_required and reason conflict, list its conflicted files. The list contains no file content, so it stays small for any number of conflicts:

TYPESHIP_DRAFT=$(curl --fail-with-body -sS "https://typeship.dev/api/v1/targets/$TYPESHIP_TARGET" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" | jq -r .draft_id)
curl --fail-with-body -sS \
  "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT/files?filter=conflicted&limit=100" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" > conflicts.json
jq '.data[] | {path, conflict, sides}' conflicts.json
FieldUse
conflict.kindWhy the merge stopped, such as overlapping_text, file_ownership, repository_deleted_incoming_changed, or no_common_version for a package with no earlier Typeship version.
conflict.sourceWhere the incoming side comes from: generation (the new Generation), default_branch (commits on the default branch), or previous_draft (code from a rebased, reset, or deleted Draft branch).
conflict.decisionThe decision saved for the current head_sha, or null.
sidesFile IDs for base (last merged), yours (repository edits), and generated (proposed new output). null means the file is absent on that side.

Continue a long list with cursor set to next_cursor. Omit filter to list customized and conflicted files together, or use filter=customized for files you changed.

Read one side of a file by its ID. Text comes back as content with encoding: utf8; binary files come back base64-encoded with encoding: base64:

file=$(jq -r '.data[] | select(.path == "src/index.ts") | .sides.generated' conflicts.json)
curl --fail-with-body -sS "https://typeship.dev/api/v1/files/$file" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" | jq -r .content

Each response holds at most 24 KiB of the file. When next_cursor is not null, repeat the request with cursor, and concatenate the chunks in order. A file ID always returns the same bytes; after a new Draft commit, list the files again for the new IDs. The Draft pull request also shows the affected files.

To keep the generated side of src/index.ts, decide against the Draft's head_sha:

head=$(curl --fail-with-body -sS "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" | jq -r .head_sha)
jq -n --arg head "$head" '{expected_head_sha: $head,
  resolutions: [{path: "src/index.ts", keep: "generated"}]}' > resolution.json
curl --fail-with-body -sS -X POST \
  "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT/resolve" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" \
  -H "Content-Type: application/json" --data-binary @resolution.json

Use keep: yours to retain your repository edit. Keeping an absent side deletes the path. The response is the Draft. Up to 1,000 conflicts can be decided together, and decisions save together or not at all. Saving again for the same conflict replaces its decision.

To combine both sides, write the final file locally and send it as text:

jq -n --arg head "$head" --rawfile content resolved-index.ts \
  '{expected_head_sha: $head,
    resolutions: [{path: "src/index.ts", keep: "content", mode: "100644", content: $content}]}' > resolution.json

Send binary files as content_base64 instead of content, and send content: null without a mode to delete the path. Final content is limited to 2 MiB per request. For a larger file, commit it to the Draft; Typeship carries the push forward, and the conflict reappears with your commit as the yours side for you to keep. A commit alone never approves a conflict. The commit that applies caller-supplied content records Resolved-by: Typeship API (or Typeship console), and its pull request lists those paths.

Saving decisions changes no files. When conflicts.decided equals conflicts.total, Typeship continues the Draft automatically and reruns checks. Deciding a path that already matches changes nothing. Another conflict may need another decision. Merge only when the Draft status is ready.

With the CLI, the same steps are typeship drafts list-files, typeship files get, and typeship drafts resolve.

Discard customizations

To return files you changed to their generated versions, list them with filter=customized, then send resolutions: [{"path":"src/helper.ts","keep":"generated"}] and the Draft's expected_head_sha to POST /drafts/{draft_id}/resolve. Selected paths revert to generated files, a Draft-only file is deleted, and unlisted paths are preserved. Submit conflict decisions and customization resets in separate requests.

A saved discard adds one commit to the Draft branch and returns the Draft with its new head_sha. Typeship carries it forward and reruns checks while the Draft status is working. If the selected files already match the generated versions, no commit is made.

Recover rewritten history

If someone rebases, force-pushes, or deletes a Draft branch, Typeship opens a new Draft branch and pull request when it receives the push or at the next Generate, and keeps the old branch. The Draft keeps its ID and starts from the code in the last Draft Typeship saw, then applies default-branch and generated changes. A conflict with source: previous_draft compares the Draft's version with that saved code. Resolve it through the same conflict action. No approval is needed.

A rewritten default branch requires your approval. The Draft has status action_required with reason history_rewritten. Its history_recovery object holds the rewritten default_sha, the Draft head_sha, and the preserved_branch that stays available. Review the affected files:

curl --fail-with-body -sS \
  "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT/files?filter=history&limit=100" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" | jq '.data[] | {path, history}'

history.change compares the rewritten default branch with the last merged package, including your edits. history.draft_differs marks files whose Draft version differs from the new default branch; recovery carries those Draft versions forward. Read base for the last merged file, yours for the rewritten default branch, and generated for the proposed recovered Draft.

Approve with the two revisions from the Draft:

curl --fail-with-body -sS "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" \
  | jq '{expected_default_sha: .history_recovery.default_sha,
         expected_head_sha: .history_recovery.head_sha}' > approval.json
curl --fail-with-body -sS -X POST \
  "https://typeship.dev/api/v1/drafts/$TYPESHIP_DRAFT/recover" \
  -H "Authorization: Bearer $TYPESHIP_TOKEN" \
  -H "Content-Type: application/json" --data-binary @approval.json

Send expected_head_sha: null when the Draft branch is absent. If either branch moved since the Draft was read, approval returns 409 resource_changed; retrieve the Draft and review again. Approval returns the Draft with history_recovery cleared and saves the recovery without changing Git. Typeship rebuilds the Draft automatically. If that work fails, the saved code remains available for a retry, and repeating the approval creates no second recovery. Resolve any conflicts before merging. Files outside this Target's directory come from the current default branch; their old Draft versions remain on the preserved branch. In the Console, the same review is Review history recovery on the Target page.

Names and structure

For OpenAPI, the first operation tag determines its resource group. A meaningful operationId determines the method name, with resource words trimmed when unambiguous: createAccount under accounts becomes client.accounts.create. Component schema names determine exported model names, subject to language conventions and reserved-name handling. See the naming rules.

Change the source Spec when you control it. For an OpenAPI source you cannot change immediately, use a reviewed Spec patch to correct a tag, operation ID, or schema name. Check the resulting API reference before accepting a rename: it can change the interface consumers compile against. GraphQL uses source edits instead of Spec patches.

Package names belong to each Target's Delivery. Configuration reference provides cli.command_name, package.go_package_name, and package metadata such as homepage and license. These controls do not provide arbitrary import paths, class hierarchies, or aliases for a legacy SDK.

Defaults and runtime options

Set shared generation defaults on Project.config and override them on Target.config where needed. Follow the canonical configuration precedence and replacement rules; retrieve the existing object and retain its keys before saving. The Generation records the effective config.

RequirementSet at generationSet by the consumer
AuthenticationOpenAPI security schemes, or the GraphQL Spec's auth settings; CLI OAuth settings in configCredentials through client options or the generated tool's documented environment variables and login flow
Shared query or header parametersglobals, using the parameter's wire nameClient defaults and per-call values; path parameters cannot be globals
Headers and request behaviorDescribe supported auth and parameters in the SpecdefaultHeaders, request hooks, or a custom transport
Retriesretries and per-operation policiesClient or per-call retry options; see precedence
PaginationDetected from the Spec; pagination pins or disables a rulePage size and cursor inputs supported by the operation

For example, this config sets shared parameters and a retry policy:

{
  "globals": ["account_id", "api-version"],
  "retries": {
    "max_retries": 3,
    "statuses": [429, 503],
    "operations": { "GET /health": { "disabled": true } }
  }
}

Use wire names and operation IDs from your Spec. Unmatched names produce generation warnings. Invalid tooling settings and package metadata are rejected when saved. See Configuration reference for pagination rules, validation, and examples.

Migrate an existing client

Generate a trial package in a separate directory and compare it with the interface customers use today. Check import paths, constructor options, method names and arguments, return values, error handling, pagination, and webhook verification.

Use Spec and config changes for differences those controls can express. Keep compatibility aliases or behavior adapters in a wrapper you own. TypeScript calls resolve to data and throw typed SDK errors; adapt callers that previously read ApiResult.ok or ApiResult.data. See results and errors.

Compile representative customer examples and run contract tests against the trial package. Document the remaining changes and publish a migration guide before replacing the official package. A Typeship compatibility verdict does not establish compatibility with a handwritten SDK it has not modeled.

Unsupported behavior

For request signing, tracing, or proxies, first check the available runtime hooks and transport options. TypeScript accepts fetch and onRequest; Python accepts transport and on_request; Go accepts WithHTTPClient and WithOnRequest. See hooks and debug logging.

For an existing webhook signing scheme, retain your verifier and parse the event only after verification succeeds. See webhook signing contracts.

If a requirement changes a generator-emitted implementation, commit the smallest reviewed edit on the Draft and add a focused check. Expect an explicit conflict when a later Generation edits the same region. If the change should apply to every Target instead, request generator support rather than repeating a long-lived patch across packages.

To retain an existing registry identity and released baseline, use Adopt an existing package. Adoption still requires you to review interface compatibility.

On this page