Indraft Custom Fields API

The custom-fields API from Indraft — 2 operation(s) for custom-fields.

GitHub OpenAPI

Operations 2

DELETE /v1/custom-fields/{id} Retire a custom field definition, keeping the values already written #
POST /v1/custom-fields Define or update a typed custom field #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/indraft-io:indraft-io-custom-fields-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

indraft-io-custom-fields-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Indraft Io Custom Fields API
  version: '1'
  description: 'Operations tagged custom-fields across 2 of this provider''s published API definitions: indraft-io-openapi.json, indraft-io.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.indraft.io
security:
- bearerAuth: []
tags:
- name: Custom Fields
paths:
  /v1/custom-fields/{id}:
    delete:
      summary: Retire a custom field definition, keeping the values already written
      description: Retire a custom field definition. Values already written stay readable and exportable, because a field somebody stopped using is history rather than a mistake. The field stops appearing in the schema, stops accepting new values, and frees its quota slot.
      tags:
      - Custom Fields
      operationId: delete_v1_custom_fields__id_
      x-indraft-capability: crm:configure
      x-indraft-scopes:
      - crm:configure
      - crm:admin
      parameters:
      - name: id
        in: path
        required: true
        description: The custom field id, as returned by the call that created or matched it.
        schema:
          type: string
      responses:
        '200':
          description: Success
          headers:
            ratelimit-limit:
              description: Requests permitted in the window closest to refusing this caller.
              schema:
                type: integer
            ratelimit-remaining:
              description: Requests left in that window. Zero means the next request is refused.
              schema:
                type: integer
            ratelimit-reset:
              description: Seconds until that window resets.
              schema:
                type: integer
            ratelimit-policy:
              description: Every window in force, not only the one reported above.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  archived:
                    type: object
                    properties:
                      id:
                        type: string
                      key:
                        type: string
                      archivedAt:
                        type: string
                    required:
                    - id
                    - key
                    - archivedAt
                    additionalProperties: true
                  mutation:
                    type: object
                    properties:
                      mutationId:
                        type: string
                      status:
                        type: string
                        enum:
                        - created
                        - updated
                        - matched
                        - archived
                        - merged
                        - reverted
                        - noop
                        description: '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:
                        type: string
                      record: {}
                      changes:
                        type: array
                        items:
                          type: object
                          properties:
                            field:
                              type: string
                              minLength: 1
                              maxLength: 128
                            before: {}
                            after: {}
                          required:
                          - field
                          additionalProperties: false
                      held:
                        type: array
                        items:
                          type: object
                          properties:
                            field:
                              type: string
                              minLength: 1
                              maxLength: 128
                            kept: {}
                            proposed: {}
                            standing:
                              type: string
                              enum:
                              - observed
                              - inferred
                              - imported
                              - user_supplied
                              - confirmed
                            incoming:
                              type: string
                              enum:
                              - observed
                              - inferred
                              - imported
                              - user_supplied
                              - confirmed
                            actorId:
                              type: string
                              minLength: 1
                            assertedAt:
                              type: string
                              minLength: 1
                          required:
                          - field
                          - standing
                          - incoming
                          - actorId
                          - assertedAt
                          additionalProperties: false
                        description: 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.
                      matchedBy:
                        type: string
                        enum:
                        - email
                        - domain
                        - external_id
                        - idempotency_key
                        - explicit_id
                      reversible:
                        type: boolean
                      replayed:
                        type: boolean
                      dryRun:
                        type: boolean
                    required:
                    - mutationId
                    - status
                    - reversible
                    additionalProperties: false
                  requestId:
                    type: string
                required:
                - archived
                - mutation
                - requestId
                additionalProperties: true
        '401':
          description: No credential, or one that has been revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: A product quota is spent, or the subscription is not active.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Authenticated, and this capability is not held.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such record, or it belongs to another workspace.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '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.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Infrastructure protection tripped. This is not a product quota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            ratelimit-limit:
              description: Requests permitted in the window closest to refusing this caller.
              schema:
                type: integer
            ratelimit-remaining:
              description: Requests left in that window. Zero means the next request is refused.
              schema:
                type: integer
            ratelimit-reset:
              description: Seconds until that window resets.
              schema:
                type: integer
            ratelimit-policy:
              description: Every window in force, not only the one reported above.
              schema:
                type: string
            retry-after:
              description: Seconds to wait before retrying. The request itself was fine.
              schema:
                type: integer
        '500':
          description: A defect on our side. The hint says so, and carries an incident id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          description: The workspace is at its storage ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    servers:
    - url: https://api.indraft.io
  /v1/custom-fields:
    post:
      summary: Define or update a typed custom field
      description: 'Define or update a typed custom field on a company, contact, opportunity, interaction, or task. A field is addressed by its object type and key. The value type cannot change once values exist. Use interaction for call disposition, meeting type and next step: those are what activity reporting is made of, and a summary is prose rather than a field you can group by. Pass `computed` to make the field DERIVED instead of stored: it is then recalculated from stored fields on every read, nothing can write it, and it can never go stale behind its inputs. Two forms only. `{form:''binary'', op:''multiply'', left:{field:''salary_minor''}, right:{field:''placement_pct_bps''}, divideBy:10000}` computes a fee from two fields and a constant. `{form:''rollup'', relationship:''opportunities'', function:''sum'', field:''amount_minor''}` totals over a declared relationship. A computed field must be a number, cannot be required or searchable, and cannot read another computed field. get_crm_schema lists the relationships and the inputs each type offers.'
      tags:
      - Custom Fields
      operationId: post_v1_custom_fields
      x-indraft-capability: crm:configure
      x-indraft-scopes:
      - crm:configure
      - crm:admin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                idempotencyKey:
                  type: string
                  minLength: 8
                  maxLength: 200
                  description: '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:
                  type: integer
                  minimum: 0
                  description: '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:
                  type: string
                  description: 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:
                  type: string
                  enum:
                  - observed
                  - inferred
                  - imported
                  - user_supplied
                  description: '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:
                  type: string
                  minLength: 1
                  maxLength: 120
                  description: '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:
                  type: boolean
                  description: 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:
                  type: boolean
                  description: '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.'
                id:
                  type: string
                key:
                  type: string
                  minLength: 1
                  maxLength: 64
                  pattern: ^[a-z][a-z0-9_]*$
                label:
                  type: string
                  minLength: 1
                  maxLength: 200
                target:
                  type: string
                  enum:
                  - company
                  - contact
                  - opportunity
                  - interaction
                  - task
                valueType:
                  type: string
                  enum:
                  - text
                  - number
                  - boolean
                  - timestamp
                  - enum
                  - json
                required:
                  type: boolean
                searchable:
                  type: boolean
                enumOptions:
                  type: array
                  items:
                    type: string
                    minLength: 1
                    maxLength: 200
                  maxItems: 100
                computed:
                  oneOf:
                  - type: object
                    properties:
                      form:
                        type: string
                        enum:
                        - binary
                      op:
                        type: string
                        enum:
                        - add
                        - subtract
                        - multiply
                        - divide
                      left:
                        anyOf:
                        - type: object
                          properties:
                            field:
                              type: string
                              minLength: 1
                              maxLength: 64
                              pattern: ^[a-z][a-z0-9_]*$
                          required:
                          - field
                          additionalProperties: false
                        - type: object
                          properties:
                            constant:
                              type: integer
                              minimum: -9007199254740991
                              maximum: 9007199254740991
                          required:
                          - constant
                          additionalProperties: false
                      right:
                        anyOf:
                        - type: object
                          properties:
                            field:
                              type: string
                              minLength: 1
                              maxLength: 64
                              pattern: ^[a-z][a-z0-9_]*$
                          required:
                          - field
                          additionalProperties: false
                        - type: object
                          properties:
                            constant:
                              type: integer
                              minimum: -9007199254740991
                              maximum: 9007199254740991
                          required:
                          - constant
                          additionalProperties: false
                      divideBy:
                        type: integer
                        minimum: -9007199254740991
                        maximum: 9007199254740991
                    required:
                    - form
                    - op
                    - left
                    - right
                    additionalProperties: false
                  - type: object
                    properties:
                      form:
                        type: string
                        enum:
                        - rollup
                      relationship:
                        type: string
                        minLength: 1
                        maxLength: 64
                        pattern: ^[a-z][a-z0-9_]*$
                      function:
                        type: string
                        enum:
                        - sum
                        - count
                        - min
                        - max
                      field:
                        type: string
                        minLength: 1
                        maxLength: 64
                        pattern: ^[a-z][a-z0-9_]*$
                    required:
                    - form
                    - relationship
                    - function
                    additionalProperties: false
                  discriminator:
                    propertyName: form
              required:
              - key
              - label
              - target
              - valueType
              additionalProperties: false
      responses:
        '200':
          description: Success
          headers:
            ratelimit-limit:
              description: Requests permitted in the window closest to refusing this caller.
              schema:
                type: integer
            ratelimit-remaining:
              description: Requests left in that window. Zero means the next request is refused.
              schema:
                type: integer
            ratelimit-reset:
              description: Seconds until that window resets.
              schema:
                type: integer
            ratelimit-policy:
              description: Every window in force, not only the one reported above.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  record:
                    type: object
                    additionalProperties: {}
                  mutation:
                    type: object
                    properties:
                      mutationId:
                        type: string
                      status:
                        type: string
                        enum:
                        - created
                        - updated
                        - matched
                        - archived
                        - merged
                        - reverted
                        - noop
                        description: '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:
                        type: string
                      record: {}
                      changes:
                        type: array
                        items:
                          type: object
                          properties:
                            field:
                              type: string
                              minLength: 1
                              maxLength: 128
                            before: {}
                            after: {}
                          required:
                          - field
                          additionalProperties: false
                      held:
                        type: array
                        items:
                          type: object
                          properties:
                            field:
                              type: string
                              minLength: 1
                              maxLength: 128
                            kept: {}
                            proposed: {}
                            standing:
                              type: string
                              enum:
                              - observed
                              - inferred
                              - imported
                              - user_supplied
                              - confirmed
                            incoming:
                              type: string
                              enum:
                              - observed
                              - inferred
                              - imported
                              - user_supplied
                              - confirmed
                            actorId:
                              type: string
                              minLength: 1
                            assertedAt:
                              type: string
                              minLength: 1
                          required:
                          - field
                          - standing
                          - incoming
                          - actorId
                          - assertedAt
                          additionalProperties: false
                        description: 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.
                      matchedBy:
                        type: string
                        enum:
                        - email
                        - domain
                        - external_id
                        - idempotency_key
                        - explicit_id
                      reversible:
                        type: boolean
                      replayed:
                        type: boolean
                      dryRun:
                        type: boolean
                    required:
                    - mutationId
                    - status
                    - reversible
                    additionalProperties: false
                  requestId:
                    type: string
                required:
                - mutation
                - requestId
                additionalProperties: true
        '401':
          description: No credential, or one that has been revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '402':
          description: A product quota is spent, or the subscription is not active.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Authenticated, and this capability is not held.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '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.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The body, or a single record inside it, exceeds its bound.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: The request does not satisfy the schema on this page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Infrastructure protection tripped. This is not a product quota.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            ratelimit-limit:
              description: Requests permitted in the window closest to refusing this caller.
              schema:
                type: integer
            ratelimit-remaining:
              description: Requests left in that window. Zero means the next request is refused.
              schema:
                type: integer
            ratelimit-reset:
              description: Seconds until that window resets.
              schema:
                type: integer
            ratelimit-policy:
              description: Every window in force, not only the one reported above.
              schema:
                type: string
            retry-after:
              description: Seconds to wait before retrying. The request itself was fine.
              schema:
                type: integer
        '500':
          description: A defect on our side. The hint says so, and carries an incident id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '507':
          description: The workspace is at its storage ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    servers:
    - url: https://api.indraft.io
components:
  schemas:
    ErrorDetail:
      type: object
      additionalProperties: true
      description: The structured payload a recovery needs. Its shape differs per code; the keys below are the ones that recur, and anything else a code carries is present alongside them.
      properties:
        fieldErrors:
          type: object
          additionalProperties:
            type: string
          description: One message per field the caller sent, keyed by that field. The key `(body)` is the request body itself, which is what an unrecognised key reports under.
        reason:
          type: string
          description: One sentence, where the refusal is about the request rather than a field. A refusal that names alternatives puts them here.
        available:
          type: array
          items:
            type: string
          description: What could have been asked for instead. Present on a filter, sort or group-by the API does not serve, so a correction is one edit rather than a round trip to read the schema.
        options:
          type: array
          items:
            type: string
          description: The values an enum accepts, on a value it does not.
        currentVersion:
          type: integer
          description: What the record's version actually is, on `stale_version`. With the value you sent it is everything needed to decide whether to re-read and retry or to stop.
        expectedVersion:
          type: integer
          description: The version you sent, echoed back on `stale_version`.
        requiredScope:
          type: string
          description: 'The capability the credential lacks. Empty nowhere: a route no credential can reach is human-only and says so in its scopes instead.'
    Error:
      type: object
      required:
      - code
      - message
      - recoveryHint
      - requestId
      properties:
        code:
          type: string
          enum:
          - unauthorized
          - forbidden_scope
          - standing_override_refused
          - workspace_not_found
          - validation_error
          - record_not_found
          - ambiguous_identity
          - duplicate_identity
          - stale_version
          - invalid_stage_transition
          - idempotency_conflict
          - not_reversible
          - merge_conflict
          - rate_limited
          - quota_exceeded
          - payload_too_large
          - workspace_capacity
          - payment_required
          - billing_state_stale
          - operation_in_flight
          - internal_error
        message:
          type: string
        recoveryHint:
          type: string
        requestId:
          type: string
        detail:
          $ref: '#/components/schemas/ErrorDetail'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: An Indraft API token, or a WorkOS-issued session or MCP access token.
x-refined-from:
- indraft-io-openapi.json
- indraft-io.json