<!-- x-generated: Generated by the Hyperscale artifact pipeline; do not edit by hand -->

# Contact attempt

> One receipted debtor contact in a collections case's regulated ledger.

A contact attempt records one debtor contact made under a collections case: the channel, the moment (stamped in the debtor's local time), and the outcome. Rows can only be recorded while the case is assigned to an agency, are verified against the case's own contact caps, and are corrected by voiding -- the ledger is append-only evidence, never mutated.

- Instrument id: `contact_attempt`
- Initial state: `recorded`
- Terminal states: `voided`
- Composed by: [b2b_trade_credit](../programs/b2b_trade_credit.md), [bnpl_checkout](../programs/bnpl_checkout.md), [dream_trip_packages](../programs/dream_trip_packages.md), [peer_lending_marketplace](../programs/peer_lending_marketplace.md), [tuition_installment_academy](../programs/tuition_installment_academy.md)

## Lifecycle

The diagram draws only the transitions a composing Product exposes as an action. A cell reading "no step any Product exposes" means no drawn edge enters or leaves that state, so the move belongs to a lifecycle step none of the composing Products exposes. Terminal states read "none" under Left by, because stopping there is the declared design rather than a missing step. Where an unexposed step carries a price it is listed under Steps no Product exposes, below.

```mermaid
stateDiagram-v2
    [*] --> recorded: create
    voided --> [*]
```

| State | Kind | Entered by | Left by |
| --- | --- | --- | --- |
| `recorded` | initial | create | no step any Product exposes |
| `voided` | terminal | no step any Product exposes | none |

## Fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `channel` | enum<sms\|email\|call\|letter> | yes |  |
| `attemptedAt` | datetime | yes |  |
| `outcome` | enum<reached\|no_answer\|promise_to_pay> | yes |  |
| `note` | text | no |  |

## References

- `caseId`: `collections_case`, required

## Actions

### create

Appends one debtor contact to the case's ledger. Admission requires the case to be assigned -- an attempt can only be evidence of agency activity while an agency actually holds the case. No money moves.

- From: none
- To: no state change
- Hints: readOnly=false, destructive=false, idempotent=true, moneyMovement=false

Exposed as:

| Program | Public action | Route |
| --- | --- | --- |
| [b2b_trade_credit](../programs/b2b_trade_credit.md) | `logContactAttempt` | `POST /v1/operations/contact_attempt.logContactAttempt`, scopes contact_attempt:write, idempotency required, receipt yes |
| [bnpl_checkout](../programs/bnpl_checkout.md) | `logContactAttempt` | `POST /v1/operations/contact_attempt.logContactAttempt`, scopes contact_attempt:write, idempotency required, receipt yes |
| [dream_trip_packages](../programs/dream_trip_packages.md) | `recordRecoveryContact` | `POST /v1/operations/contact_attempt.recordRecoveryContact`, scopes contact_attempt:write, idempotency required, receipt yes |
| [peer_lending_marketplace](../programs/peer_lending_marketplace.md) | `logOutreach` | `POST /v1/operations/contact_attempt.logOutreach`, scopes contact_attempt:write, idempotency required, receipt yes |
| [tuition_installment_academy](../programs/tuition_installment_academy.md) | `logContactAttempt` | `POST /v1/operations/contact_attempt.logContactAttempt`, scopes contact_attempt:write, idempotency required, receipt yes |

Input:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `caseId` | ref<collections_case> | yes | Collections case this attempt is ledgered under |
| `channel` | enum<sms\|email\|call\|letter> | yes |  |
| `attemptedAt` | datetime | yes |  |
| `outcome` | enum<reached\|no_answer\|promise_to_pay> | yes |  |
| `note` | text | no |  |

Effects:

- reads:
  - `reads.requires_refs` from `requiresRefs`

Errors: the 24 shared with every action on this instrument, listed once under Errors on every action.

## Reads

- `contact_attempt.list` as `contact_attempt_list`: List contact attempts
  - Route: `GET /v1/contact-attempts`, scopes contact_attempt:read, idempotency not_required, receipt yes
  - Output: items, nextCursor, statusCounts
- `contact_attempt.retrieve` as `contact_attempt_retrieve`: Retrieve a contact attempt
  - Route: `GET /v1/contact-attempts/{contactAttemptId}`, scopes contact_attempt:read, idempotency not_required, receipt yes
  - Output: contactAttemptId, tenantId, productId, instrumentId, status, fields, refs, createdAt, updatedAt

## Cost

| Action | Effect | Meter | Rate | Table version |
| --- | --- | --- | --- | --- |
| `create` | `reads.requires_refs` | `instrument.event.count` | 0 | 2026-09-02.1 |

## Errors on every action

The 24 errors every action on this instrument declares. An action's own section lists only what it adds beyond these.

| Code | HTTP | Cause | Action | Remediation | Retryable | Idempotency safe |
| --- | --- | --- | --- | --- | --- | --- |
| `instrument_aggregate_cap_exceeded` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument aggregate cap exceeded`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `instrument_aggregate_currency_mismatch` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument aggregate currency mismatch`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `instrument_reference_not_found` | 404 | No resource matches the `id` this request referenced (`instrument reference not found`). | List the collection this request referenced in the same tenant, product, and environment, then retry with an `id` that listing returns. | Create or select the resource in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `instrument_reference_required` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument reference required`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `instrument_reference_status_forbidden` | 409 | The resource's current lifecycle state does not allow this transition (`instrument reference status forbidden`). | Read the resource this request referenced and pick the lifecycle operation its current status allows. | Apply a transition the resource's current status permits. The reference itself is still valid, so no new resource is needed. | no | yes |
| `instrument_retired` | 409 | The request contradicts the resource the platform has already durably recorded (`instrument retired`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `invalid_credentials` | 401 | Hyperscale could not accept the account or credential request. Signup, login, and bearer authentication use one 401 for every case they reject, so the response exposes no credential-specific reason or registered-email verdict. The single exception is a key that is real but homed in the other environment: because the presented secret already matched a stored key, the message names the mismatch instead of leaving a working credential looking unknown. | For signup or login, re-check the submitted account details. For API calls, re-check the `Authorization: Bearer <key>` API key and whether it matches the target environment (sandbox vs live). | For signup, verify the submitted details or use login or password reset for an existing account. For login, re-enter the credentials or reset the password. For API or MCP calls, use an active key for the target environment, set HYPERSCALE_API_KEY, and retry. When the message reports an environment mismatch, `details.keyEnvironment` names where the key does work: send that environment's key, or set `X-Hyperscale-Environment` to it. | no | yes |
| `local_evidence_output_key_missing` | 500 | The platform could not durably complete the `local evidence output key missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `local_evidence_reference_missing` | 500 | The platform could not durably complete the `local evidence reference missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `local_evidence_resource_missing` | 500 | The platform could not durably complete the `local evidence resource missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `mfa_verification_required` | 403 | This tenant surface requires a session that completed TOTP at login. Tenant operations demand it while the organization's require-MFA policy (tenant.security.configure) is on; user.mfa.totp.disable demands it unconditionally, because removing a factor is the act that factor exists to guard. | Enroll and confirm a TOTP factor (user.mfa.totp.enrollment.begin, then user.mfa.totp.enrollment.confirm), then log in again and complete user.login.mfa.verify before retrying. | MFA verification is a mint-time session fact, so enrolling a factor mid-session never upgrades the current session. After the factor is active, a fresh login returns an MFA challenge; completing it mints the verified session this surface demands. Identity self-operations (logout, TOTP enrollment) stay open to a password-only session, so a factorless member is never locked out of enrolling. Disabling a factor is the exception: sign in again and complete the challenge first. | no | yes |
| `operation_insert_failed` | 500 | The platform could not durably complete the `operation insert failed` step, so the Operation may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the Operation was written. | Treat the Operation's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `operation_not_finalized` | 409 | The request contradicts the Operation the platform has already durably recorded (`operation not finalized`). | Re-read the Operation with `operation.retrieve` and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the Operation as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `product_not_found` | 404 | No Product matches the `productId` this request referenced (`product not found`). | List Products in the same tenant, product, and environment, then retry with the `productId` that read returns. | Create or select the Product in the environment the call runs against; ids never carry across tenants, or across sandbox and live. | no | yes |
| `session_expired` | 401 | The operation refused the request on its `session expired` contract condition. | Read the error details, then the operation reference for this resource, before retrying. | Correct the request against the generated SDK models and a fresh read of the resource; if the shape is already right, the product may be missing the capability this operation needs. | no | yes |
| `session_required` | 401 | The operation cannot run against this resource until the `session required` prerequisite exists. | Supply the field, capability, evidence, or configuration the error details name for this resource, then call the operation again. | Complete the prerequisite through the surface that owns it -- the operation reference for this resource names it -- and the same call then succeeds unchanged. | no | yes |
| `session_revoked` | 401 | The operation refused the request on its `session revoked` contract condition. | Read the error details, then the operation reference for this resource, before retrying. | Correct the request against the generated SDK models and a fresh read of the resource; if the shape is already right, the product may be missing the capability this operation needs. | no | yes |
| `tenant_code_missing` | 500 | The platform could not durably complete the `tenant code missing` step, so the Company may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the Company was written. | Treat the Company's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_amount_missing` | 500 | The platform could not durably complete the `usage amount missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_currency_mismatch` | 500 | The platform could not durably complete the `usage currency mismatch` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_meter_missing` | 500 | The platform could not durably complete the `usage meter missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_source_conflict` | 409 | The request contradicts the resource the platform has already durably recorded (`usage source conflict`). | Re-read the resource this request referenced and compare the submitted ids, currency, product, tenant, and expected state against what comes back. | Rebuild the request from the resource as it now stands, then retry under a NEW idempotency key -- reuse the original key only when you are deliberately replaying the original mutation. | no | yes |
| `usage_tenant_missing` | 500 | The platform could not durably complete the `usage tenant missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
| `usage_transfer_missing` | 500 | The platform could not durably complete the `usage transfer missing` step, so the resource may or may not have been written. | Capture the request id and the receipt or operation id before retrying or escalating; do not re-send under a fresh key until you know whether the resource was written. | Treat the resource's side effects as unknown unless a terminal receipt says otherwise; retry only operations documented as retryable, and replay them with the SAME idempotency key. | no | no |
