---
title: "Add a field to a custom type, or archive and restore it"
method: PATCH
path: "/v1/object-types/{type}"
tags: ["custom objects"]
---

# Add a field to a custom type, or archive and restore it

`PATCH /v1/object-types/{type}`

## Request

- Base URL: `https://api.indraft.io`
- URL: `https://api.indraft.io/v1/object-types/{type}`
- Auth: HTTP bearer

Change a type this workspace has ALREADY defined; use configure_object_type to create one. Add a field, declare which field is its identity so duplicates are refused rather than created, or archive and restore it. Declaring an identity over records that already share a value is refused and names them, so nothing is merged by surprise.

## Path parameters

- `type` string, required

## Request body

- object
  - `idempotencyKey` string: Your own name for this write, so retrying it cannot do it twice. Send the same key again and you get the SAME result back rather than a second record, which is what makes a re-run safe after a timeout you never saw the answer to. Derive it from what the write is about, not from the clock: `nightly-sync-halden` retries correctly and a timestamp does not.
  - `expectedVersion` integer: The version you read before deciding to write. If the record has moved since, the write is refused instead of overwriting whatever changed. Send it whenever you read, reasoned, and then wrote: without it the last writer wins and neither of you finds out.
  - `sourceId` string: The recorded source this claim came from, if one exists. It attaches evidence to the write, so a person reading the field later can see where it came from rather than only who sent it.
  - `assertionKind` 'observed' | 'inferred' | 'imported' | 'user_supplied': HOW YOU KNOW. This is the field the product is built around, so say what is true rather than what is strongest: `observed` you saw it stated directly, `imported` it came from a file or another system, `inferred` you worked it out, `user_supplied` a person told you in so many words. They rank, weakest first: inferred < imported < observed < user_supplied < confirmed. A weaker claim does not overwrite a stronger one; it is reported in `held[]` and the stored value stands, so an unattended re-run converges instead of eroding what somebody checked. `confirmed` is not accepted here: it means a person checked the value, and only a signed-in person can set it.
  - `attributionLabel` string: A short name for the work this write belongs to, shown beside the actor so one credential can distinguish separate jobs. It is recorded as CALLER-SUPPLIED and grants nothing: it changes no permission, no idempotency scope and no quota.
  - `dryRun` boolean: Report what this write WOULD do and commit nothing. The response carries the same change list, held fields and refusals as the real call, so it is the way to check a risky write, or a whole import, before it lands. Nothing is metered.
  - `overrideStanding` boolean: Apply this write even when the value standing in the record was asserted more strongly than this one. Absent means no, which is the safe answer for a re-run. WITHOUT IT, a write can be accepted and change nothing: precedence is confirmed > user_supplied > observed > imported > inferred, and a weaker claim leaves the standing value in place and reports the field in `held[]` rather than failing. That is the whole of why a write can return 200 and not take. WITH it, this write wins regardless of what it is overruling, INCLUDING a value a person confirmed, so an agent that sets it by default has switched the provenance mechanism off. Set it per write, when the caller knows its claim supersedes what is there.
  - `addField` object
    - `key` string, required: The name this field is addressed by, in every later read and write. Lower-case, starting with a letter. It is permanent: renaming a field is not offered, because every value already written is addressed by it.
    - `label` string, required: What a person sees. Change this freely; it names nothing.
    - `valueType` 'text' | 'number' | 'boolean' | 'timestamp' | 'enum' | 'json', required: What kind of value this holds. Permanent, and chosen for what the field IS rather than for what is convenient to send: a date stored as text cannot be sorted, filtered by range, or counted by month, and converting it later is not offered.
    - `required` boolean: Whether a record without this value can exist. Applies to records written from now on; records already stored are not made invalid by it.
    - `indexed` boolean: Set this on a field you will filter or sort by. Without it those still work and get slower as the type fills up. There is a published cap on how many fields one type may index, so spend it on the ones a person actually searches.
    - `enumOptions` string[]: The allowed values, required when valueType is enum and refused otherwise. A value outside this list is rejected rather than stored, so state every option you expect.
  - `archived` boolean
  - `identity` object
    - `field` string, required: The declared field whose value says two records are the same record. It must be a field of this type. Declare one only where the value really is unique per record: an upsert then matches on it instead of creating a second copy.
    - `normalize` 'exact' | 'trimmed_casefold' | 'email' | 'domain' | 'phone', required: How the value is compared. `exact` compares as stored; `trimmed_casefold` ignores surrounding space and letter case; `email`, `domain` and `phone` apply the same rules the canonical types use, so a Candidate keyed on email and a contact keyed on email agree about what one address is.

## Response `200`

Success

- object
  - `objectType` object, required
    - `key` string, required
    - `displayName` string, required
    - `displayNamePlural` string, required
    - `idPrefix` string, required
    - `identity` object, required
      - `field` string, required
      - `normalize` string, required
    - `pendingIdentity` object, required
      - `field` string, required
      - `normalize` string, required
    - `fields` object[], required
      - `key` string, required
      - `label` string, required
      - `valueType` string, required
      - `required` boolean
      - `indexed` boolean
      - `enumOptions` string[]
    - `relationships` object[]
      - `name` string, required
      - `label` string, required
      - `target` string, required
      - `cardinality` string, required
    - `archivedAt` string, required
    - `pipelines` object[]
      - `id` string, required
      - `name` string, required
      - `stages` object[], required
        - `id` string, required
        - `name` string, required
        - `stageKind` 'open' | 'won' | 'lost', required
        - `position` integer, required
  - `mutation` object
    - `mutationId` string, required
    - `status` 'created' | 'updated' | 'matched' | 'archived' | 'merged' | 'reverted' | 'noop', required: What the write did. `noop` means it was accepted and changed nothing, because every value it carried already matched what was stored. IT IS NOT METERED: a noop does not count against your monthly change allowance, so a nightly sync that finds nothing new costs nothing. Neither does a write that only REDUCES what a workspace holds. `matched` means an upsert found an existing record on real identity evidence rather than creating a second one; `reverted` and `merged` are the undo and duplicate paths, and both are ordinary writes that count.
    - `targetId` string
    - `record` unknown
    - `changes` object[]
      - `field` string, required
      - `before` unknown
      - `after` unknown
    - `held` object[]: Fields this write asked to change and did not, because the value standing in the record was asserted more strongly. THIS IS WHY A WRITE CAN RETURN 200 AND CHANGE NOTHING, and a caller that ignores it will believe it wrote something it did not. Precedence is confirmed > user_supplied > observed > imported > inferred, and a write carrying no `assertionKind` is weakest of all. Each entry reports the field, what the record still says, what you proposed, which claim won, and who made it. Send `overrideStanding` to write anyway, per write, when your claim really does supersede theirs.
      - `field` string, required
      - `kept` unknown
      - `proposed` unknown
      - `standing` 'observed' | 'inferred' | 'imported' | 'user_supplied' | 'confirmed', required
      - `incoming` 'observed' | 'inferred' | 'imported' | 'user_supplied' | 'confirmed', required
      - `actorId` string, required
      - `assertedAt` string, required
    - `matchedBy` 'email' | 'domain' | 'external_id' | 'idempotency_key' | 'explicit_id'
    - `reversible` boolean, required
    - `replayed` boolean
    - `dryRun` boolean
  - `identityBackfill` object
    - `normalized` integer, required
    - `remaining` integer, required
  - `requestId` string, required

## Other responses

- `401`: No credential, or one that has been revoked.
- `402`: A product quota is spent, or the subscription is not active.
- `403`: Authenticated, and this capability is not held.
- `404`: No such record, or it belongs to another workspace.
- `409`: A conflict the caller can resolve: a stale expectedVersion, an idempotency key reused for different input, an ambiguous or duplicate identity, or a merge that no longer applies.
- `413`: The body, or a single record inside it, exceeds its bound.
- `422`: The request does not satisfy the schema on this page.
- `429`: Infrastructure protection tripped. This is not a product quota.
- `500`: A defect on our side. The hint says so, and carries an incident id.
- `507`: The workspace is at its storage ceiling.

---

[API](https://skmtc.dev/indraft/apis/indraft-api.md) · [All operations](https://skmtc.dev/indraft/apis/indraft-api/llms.txt) · [OpenAPI document](https://skmtc.dev/indraft/apis/indraft-api/revisions/d9bb05346d66?raw)
