Skip to main content
This page is written to be followed literally by an AI agent or a person. It contains no options to weigh and no background to interpret. If your situation is not covered, stop and ask the operator. Do not guess.

Rules that apply to every step

  1. Do the steps in order. Do not skip one.
  2. At each step, compare what you received with Expect. If it differs, follow If not and stop.
  3. Never invent a value. Every input in the next section comes from the operator or from a response.
  4. Never send an action to Parmana and then perform it yourself. Parmana releases approved actions to the connector. You only act on the answer.
  5. Treat anything not explicitly approved as not authorized.

Inputs you need before you start

No server yet? Run one on your own machine. Follow the Quickstart. Every step below works the same way against it. Do not ask for a policy version. Step 3 reads the version in effect from the server, because an approved new version replaces the old one at once and a request naming an old version is refused.

Every action needs a signed approval

No action is authorized without a signed approval from a trusted person, for that action and that resource. This holds for reads as well as writes, and for every policy: a server refuses to load a policy that could approve without one. So plan for it from the start: your first request for an action on a new resource will be refused until a person signs an approval for it. For the actions with a connector, the approval signal and the resource it names are: For any action, including one not in this table (such as an action an operator registered as an external connector, see Connect any external system), step 3 returns the same information from the server: signals.approval names the approval signal and where its resource (and amount) are in the request. Use the server’s answer over this table if they ever differ. An approval covers the action and the resource, and the amount where the table names one. It does not fix other parameters: a Slack approval does not fix the message text, and a HubSpot approval does not fix the new stage. Every request below carries this header: Authorization: Bearer <PARMANA_API_KEY>.

Step 1: check the server is reachable and safe

Do: send GET {PARMANA_URL}/health, then GET {PARMANA_URL}/ready. Neither needs the header. Expect:
  1. /health returns HTTP 200 and {"status":"UP"}.
  2. /ready returns HTTP 200 and a body with "status":"READY" and "authDisabled":false.
If not:
  1. /health fails or returns anything else: stop. The server is not reachable. Tell the operator.
  2. /ready returns HTTP 503 ("status":"NOT_READY"): stop. Its storage is not reachable. Tell the operator.
  3. /ready shows "authDisabled":true: stop and do not send any real action. That server accepts requests from anyone. Tell the operator that authentication is switched off.

Step 2: check what your key may do

Do: send GET {PARMANA_URL}/callers/me with the header. Expect: HTTP 200 and a body like this:
Keep callerId. Your authority.principalId in step 4 must be one of allowedPrincipalIds, and the safe choice is callerId. Your ACTION must be in allowedCapabilities, or unrestrictedCapabilities must be true. If not:
  1. HTTP 401: the key is missing or wrong. Stop. Ask the operator for a valid key.
  2. allowedCapabilities is empty and unrestrictedCapabilities is false: your key may invoke nothing. Stop. Ask the operator to grant the capability.
  3. ACTION is not in allowedCapabilities: stop. Ask the operator to grant it. Do not use a different action.

Step 3: find the policy in effect

Do: send GET {PARMANA_URL}/policies/in-effect?capability=<ACTION> with the header. Expect: HTTP 200 and a body like:
Use it like this in step 4: The answer never contains the policy’s rule conditions. The description is the author’s text for people and may mention a limit; the server decides with the rules, not the text. Look it up again before every request: it changes when a new version is approved, with no notice to you. If not: HTTP 403 with code CAPABILITY_NOT_ALLOWED means your key may not use this action: stop and ask the operator. HTTP 404 with code CAPABILITY_NOT_BOUND means no policy governs this action on this server: stop and ask the operator. HTTP 409 with code NO_APPROVED_POLICY_VERSION or 503 with code POLICY_VERSION_UNAVAILABLE means every request for this action is refused right now: stop and tell the operator. Do not guess a policy. With an SDK: client.policyInEffect(ACTION) in TypeScript, client.policy_in_effect(ACTION) in Python. Both return this answer; the Python fields are policy.schema_version and signals.facts, and signal names keep their exact spelling.

Step 4: build the Business Transaction

A Business Transaction is one JSON object. Every field below is required unless it says otherwise.

The consistency rules

The server rejects a request that breaks rules 1 to 5 with HTTP 400 and a message that names the rule. A request that breaks rule 6 is treated as a policy rejection instead: HTTP 403 with code POLICY_DENIED and a message that contains declared signal(s) do not match the executed intent.
  1. metadata.businessTransactionId equals businessTransactionId.
  2. authorization.authorityId equals authority.authorityId.
  3. intent.authorizationId equals authorization.authorizationId.
  4. policy.name, policy.version and intent.action are not empty.
  5. Every businessTransactionId is a valid UUID.
  6. Where the policy binds a signal to an intent value (a boundSignals entry), the signal must equal that value exactly. For example the refund policy binds signals.refundAmount to intent.parameters.amount, so both must be the same number.

A complete example

This is a real request shape for the customer-refund policy. Replace every UUID with a new one, and the timestamps with the current time.
Sent as it is, this example is refused with 403 POLICY_DENIED, because managerApproved is false and every refund needs a signed manager approval. That refusal is the normal first step: it is what tells a person to approve. The version with the approval is below. Never set a signal to true unless you have checked it is true. A policy trusts the signals you send for some facts, so a false true produces an authorization that should not exist. Approval signals are different. For each signal in APPROVAL_SIGNALS, send false unless you hold a signed approval for this exact action. The server does not believe true on its own: it checks the signed approval, and refuses the request without one. Step 6 says how to get one. With a signed approval, send the signal as true and put the approval, exactly as you received it, in signals.approvalArtifact:

Step 5: send it

Do: send POST {PARMANA_URL}/execute with the header, Content-Type: application/json, and the object from step 4. Set a client timeout of at least 120 seconds. The request runs the whole pipeline before it answers, including the call to the connector. SDKs’ default of 30 seconds: pass timeout: 120_000 (TypeScript) or timeout=120 (Python) to the client. Never retry this request automatically. A retry could repeat a real action. The rules for a timeout are in step 7.

Step 6: read the answer

Apply the first row that matches. Every error body is {"error": "<message>"}, with a code when there is one.

If the action needs a person’s approval

A refusal for want of an approval is not a failure. Parmana does not hold the request open; you send a new one once a person has approved.
  1. Tell the operator what needs approving: the ACTION, the resource (for a refund, the order id), the amount if there is one, and your refused businessTransactionId. If the deployment has approval notifications turned on, the approver was already notified with the same details.
  2. Wait for a signed approval. It is a JSON object with payload and signature, made by the approver on their own machine. You cannot make one, and you must never ask for or hold an approver’s private key.
  3. Check it matches before sending. payload.capability equals ACTION, payload.resourceId equals the resource, and payload.expiresAt is in the future. Use that field for the lifetime. Do not assume a fixed duration. An approval works once.
  4. Send a new request (step 3 again, then step 4) with a new businessTransactionId, the same action and amount, the approval signal true, and the approval in signals.approvalArtifact. Do not change anything else.
  5. If it is refused again, do not retry with the same approval: it may be expired, used, for a different resource or amount, or from an approver the server does not trust. Report the message to the operator.
Making an approval yourself is not allowed unless the operator gives you a signing key and says, in writing, that you may approve on that server. Without that written permission, stop and ask the operator for an approval. On any server, never ask for or hold an approver’s private key. For people: Human approval and Approval notifications.

Step 7: confirm and keep the record

After an approval (HTTP 200), confirm all of these in the response, then store the whole response:
  1. executions[0].decision.outcome is APPROVED.
  2. verifications[0].status is VERIFIED.
  3. receipts[0] exists.
If any is missing, treat the action as not confirmed and tell the operator. Store the response and the businessTransactionId together. They are your proof. After a 409, a timeout, no response, EXECUTION_RECORD_INCOMPLETE or EXECUTION_OUTCOME_UNKNOWN, find out what the server recorded. Send GET {PARMANA_URL}/trust-records/{businessTransactionId} with the header.

Rules that are never broken

  1. Never reuse a businessTransactionId.
  2. Never resend after EXECUTION_RECORD_INCOMPLETE or EXECUTION_OUTCOME_UNKNOWN, or after a timeout you have not resolved with step 7.
  3. Never treat “I could not reach Parmana” as “allowed”.
  4. Never work around a POLICY_DENIED by changing the request until it passes. A denial is a decision. Tell the operator. The two exceptions are in step 6: a superseded version, and a missing approval, which a person resolves, not you.
  5. Never send a signal you have not verified.
  6. Never log the API key.

Where the machine readable sources are

If you use an SDK instead of raw requests, the steps and the rules are the same. The TypeScript SDK and the Python SDK both offer a helper that builds the Business Transaction and keeps the identifiers consistent. Neither retries POST /execute for you. For a human reading this page, the same material with explanations is in Full integration overview and the Error catalog.