---
title: "Upsert Credential"
method: PUT
path: "/credentials/{system}/{account_key}"
tags: ["credentials"]
---

# Upsert Credential

`PUT /credentials/{system}/{account_key}`

Store or rotate one credential.

A rotation replaces the whole login; there is no endpoint that reads one back,
by construction rather than by permission — a lost password is re-entered, not
recovered.

## Path parameters

- `system` 'servicetitan' | 'aspire' | 'housecall_pro' | 'portal' | 'rebates' | 'permit' | 'other', required
- `account_key` string, required

## Request body

- CredentialWrite — Inbound payload for creating or rotating one credential. Shaped like a password-manager login item: a name the customer recognizes, the username, the site it signs into, the password, and an optional MFA seed.
  - `label` string, nullable — The customer's name for this login
  - `username` string, nullable — Account username; stored plaintext, not a secret
  - `login_url` string, required — Where this login is used; required, and drives the site icon
  - `password` string, password, nullable — Account password; encrypted
  - `totp_secret` string, password, nullable — Base32 TOTP seed; encrypted, optional
  - `extra_secrets` object — Any further named secrets this sign-in needs — a company code, a PIN, a second passphrase. Encrypted alongside the password; the NAMES are stored in the clear so the list can show what a credential holds.

## Response `200`

Successful Response

- CredentialSummary — One credential as the dashboard sees it — identity and health, never a secret. Also carries the credentials NOT yet in the new store (1Password refs, legacy inline fields), so the list is a complete inventory of what NOSO holds for this organization rather than only what was typed into the dashboard.
  - `id` string, nullable — Null for a credential not yet in the store
  - `organization_id` string, required
  - `system` string, required
  - `account_key` string, required
  - `label` string, nullable
  - `username` string, nullable
  - `login_url` string, nullable
  - `field_names` string[] — Which secrets are held
  - `storage` 'encrypted' | 'onepassword' | 'legacy_plaintext'
  - `managed_by` string — 'customer' or 'noso_ops'
  - `status` string
  - `created_by` string, nullable
  - `created_at` string, date-time, nullable
  - `last_rotated_at` string, date-time, nullable
  - `last_used_at` string, date-time, nullable — Last successful unwrap by Pilot

## Other responses

- `404` — Not found
- `422` — Validation Error

## Changes

- **2026-09-11** `07ae211dc96b` — 1 info
  - added the new enum value `rebates` to the `path` request parameter `system`
- **2026-09-10** `8eb1d6b3ce25` — 1 info
  - added the new optional request property `extra_secrets`
- **2026-09-09** `a786fa3c27df` — 1 info
  - endpoint added

[Change history](https://skmtc.dev/noso/apis/crucible-backend-api/changes/credentials/:system/:account_key/put.md)

---

[API](https://skmtc.dev/noso/apis/crucible-backend-api.md) · [All operations](https://skmtc.dev/noso/apis/crucible-backend-api/llms.txt) · [OpenAPI document](https://skmtc.dev/noso/apis/crucible-backend-api/revisions/6242f8ed3b67?raw)
