> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Propose registering or revoking a business signal source

> Proposes connecting a business system, any system that owns facts a policy needs (an ERP, an order system, a CRM, a ledger), so Parmana can ask it instead of trusting the agent's claim.



## OpenAPI

````yaml openapi.bundled.yaml POST /business-signal-sources/changes
openapi: 3.1.0
info:
  title: Parmana API
  version: 1.0.0
  description: >
    Parmana sits between an AI agent and the systems it acts on. An agent sends
    a Business Transaction: who

    authorized it, what action it wants, and the facts that justify it. Parmana
    evaluates the policy bound to

    the action, checks any signed human approval the policy requires, releases
    the action to the connector

    only if the policy approves, and returns a signed Execution Trust Record
    anyone can verify.


    **Authentication.** Send `Authorization: Bearer <key>` on every request. The
    only routes that need no key

    are the probes, the documents and the independent verification routes: `GET
    /health`, `GET /ready`,

    `GET /openapi.yaml`, `GET /openapi.json`, `GET /api-manifest.json`, `GET
    /documentation`,

    `GET /reference`, `GET /parmana-handbook.pdf`, `GET
    /handbook/download-leads`,

    `POST /handbook/download-leads`, `POST /refusal/verify`, `POST
    /execution-intents/verify`,

    `POST /audit/verify`, `GET /keys/{keyId}` and `GET /.well-known/jwks.json`.
    A missing or unknown key

    returns `401` before anything else runs. The server holds only a hash of
    each key. See

    [Authentication](/api-reference/authentication).


    **Two kinds of key.** An agent or service key may invoke only the
    capabilities listed on it. A human key

    (`credentialHolderType: USER`) is required for governance: proposing and
    approving policies, approvers

    and external connectors, and repairing Execution Intents. Approving or
    rejecting a change also needs a

    step up authorization signed with the checker's own step up key, and the
    checker must not be the

    person who proposed it.


    **Ownership.** Every lookup by `businessTransactionId` is scoped to the key
    that submitted the

    transaction. A transaction submitted by another key returns the same `404`
    as one that does not exist.


    **Try it live.** The first server below is the public sandbox,
    `https://parmana-sandbox.vercel.app`: the same

    code as production with its own database, keys and demo approver, whose one
    action, `sandbox:receipt`,

    acts on nothing. Every "Try it" panel calls it with the published demo key
    prefilled. The demo key may

    invoke only `sandbox:receipt`, so other capabilities return `403` and
    governance routes return

    `403 NON_HUMAN_CALLER_DENIED`. Everything sent to the sandbox is visible to
    every visitor. See

    [Playground](/playground). Production needs a key issued by its operator.


    **Start here:** the [Playground](/playground) (no install), the
    [Quickstart](/quickstart), then [Integrate Parmana](/agents/integrate) for
    agents

    and [Connect any external system](/guides/connect-any-external-system) for
    your own endpoint.
  contact:
    name: Parmana Systems
    email: founder@parmanasystems.com
  license:
    name: Proprietary, source-available for evaluation only, see LICENSE
    url: https://github.com/pavancharak/parmana/blob/main/LICENSE
servers:
  - url: https://parmana-sandbox.vercel.app
    description: >-
      Public sandbox (the docs playground). Same code as production, demo data
      only, acts on nothing. Demo key prefilled.
  - url: https://parmana-api-real.vercel.app
    description: Production. Needs a key issued by the operator.
  - url: http://localhost:3000
    description: Local (packages/api, PORT env var, default 3000)
security:
  - bearerAuth: []
tags:
  - name: Execution
    description: >-
      Executes a Business Transaction through the complete Execution Trust
      pipeline
  - name: Transactions
    description: Business Transaction creation and retrieval
  - name: Verification
    description: Deterministic verification of an Execution Trust Record
  - name: Receipts
    description: Cryptographically signed Execution Trust Receipts
  - name: Trust Records
    description: Execution Trust Record retrieval
  - name: Replay
    description: Deterministic replay of a recorded Execution Trust Record
  - name: Policies
    description: Policy existence/readability check
  - name: Policy Governance
    description: Maker-checker proposal, listing, approval, and rejection of policy changes
  - name: Approvers
    description: Maker-checker for the keys trusted to sign approvals, without a deploy
  - name: External Connectors
    description: >-
      Maker-checker for binding a capability to an operator's HTTPS endpoint and
      its policy, without a deploy (ADR-0013)
  - name: Business Signal Sources
    description: >-
      Maker-checker for the business systems Parmana asks for the facts a policy
      needs, without a deploy (RFC-0023)
  - name: Authority Grants
    description: >-
      Maker-checker for which agent may have which action decided, within which
      limits, until when (RFC-0023)
  - name: Sandbox
    description: >-
      Routes that exist only on the public sandbox (ADR-0014), for trying
      Parmana from a browser
  - name: Refusal Records
    description: >-
      Durable, signed evidence that a policy decision rejected a transaction
      (RFC-0021)
  - name: Execution Intents
    description: >-
      A signed statement, stored before an action is released, of exactly what
      is about to be released, plus the operator tools to find and repair a
      released action that has no signed Trust Record (ADR-0012)
  - name: Audit
    description: >-
      Signed caller-authentication audit events, independently
      third-party-verifiable
  - name: System
    description: Operational endpoints
paths:
  /business-signal-sources/changes:
    post:
      tags:
        - Business Signal Sources
      summary: Propose registering or revoking a business signal source
      description: >
        Proposes connecting a business system, any system that owns facts a
        policy needs (an ERP, an

        order system, a CRM, a ledger), so Parmana can ask it instead of
        trusting the agent's claim.

        `register` names the source, the HTTPS endpoint Parmana asks, optionally
        a timeout, and optionally

        the Ed25519 public key the source signs its answers with; `revoke` ends
        the active registration

        for a name. Nothing changes until a different person approves it. The
        endpoint must be `https`,

        name a domain (not an IP address or localhost), carry no user name,
        password or fragment, and

        resolve only to public addresses; the address is checked again on
        approval and at every query.

        To move a source, revoke it, then register the new endpoint. Needs a
        human key.
      operationId: proposeBusinessSignalSourceChange
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: >-
                #/components/schemas/propose-business-signal-source-change-request.schema
            examples:
              register:
                summary: Real captured request
                value:
                  action: register
                  name: erp
                  endpointUrl: https://orders.example.com/parmana/signals
                  timeoutMs: 5000
                  publicKeyPem: |
                    -----BEGIN PUBLIC KEY-----
                    MCowBQYDK2VwAyEAsgWd/PtRf9OsENif9bgJegghUpmEOOZN5NROxc3zpPc=
                    -----END PUBLIC KEY-----
                  keyId: erp-2026-10
                  reason: Release goods only against invoices the ERP says are paid.
              revoke:
                summary: Real captured request
                value:
                  action: revoke
                  name: erp
                  reason: The ERP moves to a new host.
      responses:
        '201':
          description: Proposed and waiting for a checker. Keep the `changeId`.
          content:
            application/json:
              schema:
                $ref: >-
                  #/components/schemas/business-signal-source-change-response.schema
              examples:
                register:
                  summary: Real captured response
                  value:
                    changeId: 5ba7d3a6-d56d-4e90-b588-402d44aeee0c
                    action: register
                    name: erp
                    endpointUrl: https://orders.example.com/parmana/signals
                    timeoutMs: 5000
                    publicKeyPem: >
                      -----BEGIN PUBLIC KEY-----

                      MCowBQYDK2VwAyEAsgWd/PtRf9OsENif9bgJegghUpmEOOZN5NROxc3zpPc=

                      -----END PUBLIC KEY-----
                    keyId: erp-2026-10
                    reason: Release goods only against invoices the ERP says are paid.
                    proposedBy: source-maker
                    proposedAt: '2026-10-11T02:58:24.725Z'
                    status: PENDING_APPROVAL
                revoke:
                  summary: Real captured response
                  value:
                    changeId: 48579135-1b91-4ac1-ade4-245e7ca304a3
                    action: revoke
                    name: erp
                    reason: The ERP moves to a new host.
                    proposedBy: source-maker
                    proposedAt: '2026-10-11T02:58:24.789Z'
                    status: PENDING_APPROVAL
        '400':
          description: >-
            `INVALID_BUSINESS_SIGNAL_SOURCE`: a field is missing or malformed,
            the key is not Ed25519, a keyId comes without a key, or a revoke
            carries registration fields. `EXTERNAL_ENDPOINT_ADDRESS_REFUSED`:
            the endpoint is not https, names an IP address or localhost,
            resolves to an address that is not public, or does not resolve.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/error.schema'
              examples:
                badName:
                  summary: Real captured response
                  value:
                    error: >-
                      name must match ^[a-z0-9][a-z0-9-]{0,62}$, the form a
                      policy's signalSources uses.
                    code: INVALID_BUSINESS_SIGNAL_SOURCE
                notEd25519:
                  summary: Real captured response
                  value:
                    error: publicKeyPem must be an Ed25519 key.
                    code: INVALID_BUSINESS_SIGNAL_SOURCE
                keyIdWithoutKey:
                  summary: Real captured response
                  value:
                    error: >-
                      keyId labels a publicKeyPem; give the key too, or leave
                      keyId out.
                    code: INVALID_BUSINESS_SIGNAL_SOURCE
                badTimeout:
                  summary: Real captured response
                  value:
                    error: >-
                      timeoutMs must be an integer from 1000 to 30000; it
                      defaults to 10000.
                    code: INVALID_BUSINESS_SIGNAL_SOURCE
                revokeWithRegistrationFields:
                  summary: Real captured response
                  value:
                    error: A revoke takes only action, name and reason.
                    code: INVALID_BUSINESS_SIGNAL_SOURCE
                plainHttp:
                  summary: Real captured response
                  value:
                    error: endpointUrl must use https.
                    code: EXTERNAL_ENDPOINT_ADDRESS_REFUSED
                privateAddress:
                  summary: Real captured response
                  value:
                    error: >-
                      The endpoint host 'internal.example.com' resolves to an
                      address that is not public (10.0.0.7).
                    code: EXTERNAL_ENDPOINT_ADDRESS_REFUSED
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`NON_HUMAN_CALLER_DENIED`: this needs a human key.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/error.schema'
              examples:
                service:
                  summary: Real captured response, a service credential
                  value:
                    error: >-
                      This action requires a caller credential provisioned as a
                      verified human (credentialHolderType: USER).
                    code: NON_HUMAN_CALLER_DENIED
        '409':
          description: >-
            `CONFLICT`: the name already has an active registration (to
            register) or has none (to revoke), or another change for it is
            pending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/error.schema'
              examples:
                pending:
                  summary: Real captured response
                  value:
                    error: >-
                      A change for business signal source 'erp' is already
                      pending. It must be approved or rejected first.
                    code: CONFLICT
                alreadyActive:
                  summary: Real captured response
                  value:
                    error: >-
                      Business signal source 'erp' is already registered. Revoke
                      it first.
                    code: CONFLICT
                revokeUnregistered:
                  summary: Real captured response
                  value:
                    error: Business signal source 'crm' is not registered.
                    code: CONFLICT
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >-
            # erp-answers.public.pem: the Ed25519 public key the ERP signs its
            answers with.

            curl -X POST
            https://parmana-api-real.vercel.app/business-signal-sources/changes
            \
              -H "Authorization: Bearer $PARMANA_API_KEY" \
              -H "Content-Type: application/json" \
              --data "$(jq -n --rawfile key erp-answers.public.pem '{
                action: "register",
                name: "erp",
                endpointUrl: "https://orders.example.com/parmana/signals",
                timeoutMs: 5000,
                publicKeyPem: $key,
                keyId: "erp-2026-10",
                reason: "Release goods only against invoices the ERP says are paid."
              }')"
        - lang: typescript
          label: TypeScript
          source: |-
            import { ParmanaClient } from "@parmana/sdk";
            import { readFileSync } from "node:fs";

            const client = new ParmanaClient({
              endpoint: "https://parmana-api-real.vercel.app",
              apiKey: process.env.PARMANA_API_KEY!,
            });

            const change = await client.proposeBusinessSignalSourceChange({
              action: "register",
              name: "erp",
              endpointUrl: "https://orders.example.com/parmana/signals",
              timeoutMs: 5000,
              publicKeyPem: readFileSync("erp-answers.public.pem", "utf8"),
              keyId: "erp-2026-10",
              reason: "Release goods only against invoices the ERP says are paid.",
            });

            console.log(change.changeId);
        - lang: python
          label: Python
          source: |-
            import os
            from pathlib import Path

            from parmana import ParmanaClient

            client = ParmanaClient(
                endpoint="https://parmana-api-real.vercel.app",
                api_key=os.environ["PARMANA_API_KEY"],
            )

            change = client.business_signal_sources.propose_register(
                name="erp",
                endpoint_url="https://orders.example.com/parmana/signals",
                timeout_ms=5000,
                public_key_pem=Path("erp-answers.public.pem").read_text(),
                key_id="erp-2026-10",
                reason="Release goods only against invoices the ERP says are paid.",
            )

            print(change.change_id)
components:
  schemas:
    propose-business-signal-source-change-request.schema:
      title: Propose Business Signal Source Change Request
      description: >-
        Request body for POST /business-signal-sources/changes. A register
        carries endpointUrl and optionally timeoutMs, publicKeyPem and keyId; a
        revoke carries none of them.
      type: object
      additionalProperties: true
      required:
        - action
        - name
        - reason
      properties:
        action:
          type: string
          enum:
            - register
            - revoke
        name:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]{0,62}$
        endpointUrl:
          type: string
          maxLength: 2048
          description: Required for register. https only, a public host name.
        timeoutMs:
          type: integer
          minimum: 1000
          maximum: 30000
          default: 10000
        publicKeyPem:
          type: string
          maxLength: 1000
          description: >-
            Optional, register only. An Ed25519 public key in PEM. Recommended:
            without it, Parmana trusts the answer on the strength of the pinned
            HTTPS connection alone.
        keyId:
          type: string
          pattern: ^[A-Za-z0-9._-]{1,128}$
          description: Optional, register only, and only with publicKeyPem.
        reason:
          type: string
          minLength: 1
          maxLength: 2000
      examples:
        - action: register
          name: erp
          endpointUrl: https://orders.example.com/parmana/signals
          timeoutMs: 5000
          publicKeyPem: |
            -----BEGIN PUBLIC KEY-----
            MCowBQYDK2VwAyEAsgWd/PtRf9OsENif9bgJegghUpmEOOZN5NROxc3zpPc=
            -----END PUBLIC KEY-----
          keyId: erp-2026-10
          reason: Release goods only against invoices the ERP says are paid.
        - action: revoke
          name: erp
          reason: The ERP moves to a new host.
    business-signal-source-change-response.schema:
      $ref: '#/components/schemas/business-signal-source-change.schema'
      title: Business Signal Source Change Response
      description: >-
        Response of POST /business-signal-sources/changes (201) and of approve
        and reject (200). A bare Business Signal Source Change, no wrapper.
    error.schema:
      title: Error Response
      description: >-
        Shared error envelope produced by
        packages/api/src/middleware/error-handler.ts and by every route's inline
        validation checks. error is always a plain human-readable string (never
        a nested object). code is present only when the failure was a
        RuntimeError subclass reaching the centralized error handler
        (VerificationFailedError, ReceiptGenerationError, or an uncategorized
        RuntimeError); it is absent from every inline route-level check
        (businessTransactionId format/required checks) and from
        BusinessTransactionValidationError, PolicyValidationError,
        SignalValidationError, PolicyNotFoundError,
        DuplicateBusinessTransactionError, and the generic 500 fallback. POST
        /policies/validate does NOT use this envelope at all. See its own
        response schema. For the triggering condition and recommended caller
        action behind any specific error/code/status combination, see the Error
        catalog at /api-reference/error-catalog.
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.
        code:
          type: string
          description: >-
            Stable machine-readable error code. Only present for errors that
            reach the handler as a RuntimeError.
          examples:
            - RUNTIME_ERROR
            - VERIFICATION_FAILED
            - RECEIPT_GENERATION_FAILED
            - POLICY_DENIED
            - CAPABILITY_NOT_ALLOWED
            - RATE_LIMITED
            - AUDIT_UNAVAILABLE
            - SIGNING_UNAVAILABLE
            - CONNECTOR_NOT_REGISTERED
            - EXECUTION_RECORD_INCOMPLETE
            - EXECUTION_INTENT_UNAVAILABLE
            - EXECUTION_INTENT_RESULT_NOT_RECORDED
            - EXECUTION_INTENT_NOT_FOUND
            - EXECUTION_INTENTS_NOT_ENABLED
            - EXECUTION_INTENT_NOT_RESOLVABLE
            - EXECUTION_INTENT_RESOLUTION_INVALID
            - NON_HUMAN_CALLER_DENIED
            - EXECUTION_OUTCOME_UNKNOWN
            - AUTHORIZATION_NO_LONGER_VALID
        executionStatus:
          type: string
          enum:
            - EXECUTION_UNKNOWN
            - NOT_EXECUTED
          description: >-
            RFC-0023 phase 4. EXECUTION_UNKNOWN with EXECUTION_OUTCOME_UNKNOWN:
            the action was released and the call failed, so whether it ran is
            unknown; never reported as failed, and the Execution Intent stays
            ERRORED until a person resolves it. NOT_EXECUTED with
            AUTHORIZATION_NO_LONGER_VALID: the release was refused because a
            business condition or the authority grant it rested on no longer
            held, and nothing was sent.
      examples:
        - error: businessTransactionId must be a valid UUID.
        - error: >-
            Business Transaction 'eed2a972-1bf5-4166-8472-761f76fbf1b2' already
            exists.
        - error: >-
            Execution rejected: Vendor payment rejected because the assessed
            payment risk exceeds the maximum permitted threshold.
          code: RUNTIME_ERROR
    business-signal-source-change.schema:
      title: Business Signal Source Change
      description: >-
        A proposal to register a business signal source, or to revoke the active
        one for a name, and its resolution (RFC-0023 phase 2). One person
        proposes it; a different person approves or rejects it with a step up
        authorization. Only an approved change affects which sources Parmana
        asks.
      type: object
      additionalProperties: false
      required:
        - changeId
        - action
        - name
        - reason
        - proposedBy
        - proposedAt
        - status
      properties:
        changeId:
          type: string
          description: >-
            Unique id of the change, a UUID. The step up authorization for
            approve or reject names it in payload.pendingPolicyChangeId. An
            approved register change's id is also the registrationId.
        action:
          type: string
          enum:
            - register
            - revoke
          description: >-
            register names a source and its endpoint; revoke ends the active
            registration for the name.
        name:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]{0,62}$
          description: The name a policy's signalSources uses in `source`.
        endpointUrl:
          type: string
          description: >-
            register only. The endpoint as the server stored it, normalized by
            URL parsing: https, a host name with a domain, no IP literal, no
            localhost, no user name, password or fragment, resolving only to
            public addresses.
        timeoutMs:
          type: integer
          minimum: 1000
          maximum: 30000
          description: >-
            register only. How long Parmana waits for the source's answer.
            Defaults to 10000.
        publicKeyPem:
          type: string
          description: >-
            register only, optional. The Ed25519 public key the source signs its
            answers with, as the server stored it (SPKI PEM).
        keyId:
          type: string
          pattern: ^[A-Za-z0-9._-]{1,128}$
          description: register only, optional, and only with publicKeyPem.
        reason:
          type: string
          maxLength: 2000
          description: Why, from the proposer.
        proposedBy:
          type: string
          description: The proposer's caller id. Always a human credential.
        proposedAt:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - PENDING_APPROVAL
            - APPROVED
            - REJECTED
          description: >-
            PENDING_APPROVAL until a second person approves or rejects it; then
            APPROVED or REJECTED, once.
        resolvedBy:
          type: string
          description: Who approved or rejected it. Never the proposer.
        resolvedAt:
          type: string
          format: date-time
        rejectionReason:
          type: string
          description: Present when REJECTED.
      examples:
        - changeId: 5ba7d3a6-d56d-4e90-b588-402d44aeee0c
          action: register
          name: erp
          endpointUrl: https://orders.example.com/parmana/signals
          timeoutMs: 5000
          publicKeyPem: |
            -----BEGIN PUBLIC KEY-----
            MCowBQYDK2VwAyEAsgWd/PtRf9OsENif9bgJegghUpmEOOZN5NROxc3zpPc=
            -----END PUBLIC KEY-----
          keyId: erp-2026-10
          reason: Release goods only against invoices the ERP says are paid.
          proposedBy: source-maker
          proposedAt: '2026-10-11T02:58:24.725Z'
          status: APPROVED
          resolvedBy: source-checker
          resolvedAt: '2026-10-11T02:58:24.745Z'
  responses:
    Unauthorized:
      description: >-
        The `Authorization` header is missing, is not a bearer key, or names a
        key the server does not know. Nothing ran. Send a valid key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/error.schema'
          examples:
            authRequired:
              summary: Real captured response, missing or invalid Authorization header
              value:
                error: authentication required
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      x-default: 2VfYWCzt_cBAPK-8uufX6ordfY2JuQhFPsohuEumKME
      description: >
        Caller API key issued by scripts/generate-api-key.ts. Sent as
        Authorization: Bearer <key>. Verified against a stored SHA-256 hash in
        constant time by packages/api/src/auth/StaticKeyAuthenticator.ts.
        Required on every route not listed as exempt in this document's
        top-level description. See /api-reference/authentication.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.