Custody Permits

Create and manage custody permits (authorizations) for vehicle custody transfers.

API Resource Name

Custody permits are accessed via client.authorizations.* methods. The API resource is named "authorizations" to distinguish it from authentication.

Overview

Custody permits authorize specific parties to take custody of vehicles. Each authorization specifies:

  • VIN: The vehicle being authorized
  • Route: Origin and destination locations
  • Person Identity: Who can take custody (from VECU IDV SDK)
  • Role: Owner, admin, or driver
  • Validity Period: When the authorization is valid

Multi-driver authorization (ADR-042)

An authorization can carry one driver (the default single_driver mode) or a list of authorized drivers that resolves to one assigned driver later (multi_driver mode). Multi-driver mode is opt-in per client and introduces three operations beyond the single-driver shape: an extended create() accepting an authorized_drivers list, update_drivers() to add or remove drivers, and assign_to_driver() to lock the authorization to one driver. See the Multi-Driver Integration Guide for end-to-end flows.

Methods

create()

Create a new custody authorization for a vehicle.

vinstrrequired

17-character Vehicle Identification Number (excludes I, O, Q)

originstrrequired

Origin location ID (format: LOC-{TYPE}-{ID})

destinationstrrequired

Destination location ID (format: LOC-{TYPE}-{ID})

person_identity_keystrrequired

Person identity from VECU IDV SDK (10-256 characters)

authorized_bystrrequired

System or user initiating the request

make_modelstrrequired

Vehicle make and model (e.g., "Honda Accord 2023")

roleAuthorizationRole

Authorization role: OWNER, ADMIN, or DRIVER. Default: DRIVER

buyer_identity_keystr

Buyer identity key (optional)

transport_order_idstr

Transport order ID (optional)

permitted_actionslist[str]

List of permitted actions (default: PICKUP, CHECKPOINT, DELIVERY)

valid_untildatetime

Authorization expiration (default: 24 hours from creation)

authorized_driverslist[AuthorizedDriver]

Multi-driver mode (ADR-042) — list of drivers eligible to take custody; one will be locked in via assign_to_driver() later. Mutually exclusive with person_identity_key — supplying both returns 400 CONFLICTING_DRIVER_FIELDS; supplying neither returns 400 MISSING_DRIVER. Requires the calling client to be configured for multi_driver mode (otherwise 400 MULTI_DRIVER_MODE_REQUIRED). Bounded by per-client cap (default 25, configurable down per-client but not up — exceeding returns 429 DRIVER_LIMIT_EXCEEDED).

actor_identifierstr

Caller-asserted forensic identifier for the internal user inside your system that initiated the request (ADR-042 §Decision §9). Stored verbatim and emitted on custody.authorization.created, .modified, and .assigned events; NOT verified or validated by VECU. Useful when your client serves multiple internal users and you want a correlation hook into your own audit logs.

Returns: Authorization object with authorization_id, status, version (multi-driver only — monotonic version for If-Match ETag concurrency on later update_drivers() / assign_to_driver() calls), driver_authorization_mode, etc.

Raises:

  • DuplicateAuthorizationError — 409, an active authorization already exists for (VIN, origin) regardless of client_id or organization_id. ADR-042 enforces strict uniqueness at the (VIN, geohash) marker; cross-tenant collisions deny the existing authorization details (no existing_authorization_id returned).
  • PoolFanOutError — 503 POOL_FANOUT_FAILURE, multi-driver POST fan-out into the authorization pool failed partway and was rolled back. Retry the entire POST — idempotent at (VIN, origin). Body carries succeeded_driver_count, rolled_back_entries, and rollback_failures (any composite IDs left orphaned at auth-pool that need ops attention).
  • MultiDriverModeRequiredError — 400, the client is configured for single_driver mode but the request supplied authorized_drivers. Subclass of InvalidConfigurationError. Contact your VECU administrator to opt the client into multi-driver mode.
  • DriverLimitExceededError — 429, the requested authorized_drivers list exceeds the per-client cap (default 25). NO state changes applied. Body carries attempted_count and max_allowed.
  • ValidationError — 400, e.g., both person_identity_key AND authorized_drivers supplied (CONFLICTING_DRIVER_FIELDS), or neither supplied (MISSING_DRIVER).

Example (single-driver — unchanged from ADR-040 era):

from vecu_custody import CustodyClient

client = CustodyClient.sandbox(token="your-jwt-token")

# Create an authorization
auth = client.authorizations.create(
    vin="9HGBH41JXMN999999",
    origin="LOC-AUCTION-MANHEIM-ATLANTA",
    destination="LOC-DEALERSHIP-CARMAX-ORLANDO",
    person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",
    authorized_by="system-auction-integration",
    make_model="Honda Accord 2023"
)

print(f"Authorization ID: {auth.authorization_id}")
print(f"Status: {auth.status}")
print(f"Expires: {auth.expires_at}")

Example (multi-driver — ADR-042):

from vecu_custody import CustodyClient
from vecu_custody.models import AuthorizedDriver
from vecu_custody.exceptions import (
    DriverLimitExceededError,
    MultiDriverModeRequiredError,
    PoolFanOutError,
)

client = CustodyClient.sandbox(token="your-jwt-token")

# Create a multi-driver authorization with three eligible drivers
try:
    auth = client.authorizations.create(
        vin="9HGBH41JXMN999999",
        origin="LOC-AUCTION-MANHEIM-ATLANTA",
        destination="LOC-DEALERSHIP-CARMAX-ORLANDO",
        authorized_drivers=[
            AuthorizedDriver(
                person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",
                name="Alice Driver",
            ),
            AuthorizedDriver(
                person_identity_key="vecu_kMxLWqPrBn4-aT2bCdEf5gHi",
                name="Bob Driver",
            ),
            AuthorizedDriver(
                person_identity_key="vecu_pQrStUvWx-9876yZa",
                name="Carol Driver",
            ),
        ],
        authorized_by="system-auction-integration",
        make_model="Honda Accord 2023",
        actor_identifier="ops-user-42@client.example.com",
    )

    # Multi-driver responses include a monotonic version (use as ETag on
    # later modifications) and the driver authorization mode stamped at
    # creation time.
    print(f"Authorization ID: {auth.authorization_id}")
    print(f"Mode: {auth.driver_authorization_mode}")  # "multi_driver"
    print(f"Version: {auth.version}")  # 1
    print(f"Drivers on the list: {len(auth.authorized_drivers)}")
except MultiDriverModeRequiredError:
    # Client is configured for single_driver mode — coordinate with VECU
    # to opt in before retrying.
    print("Client not configured for multi-driver mode")
except DriverLimitExceededError as e:
    print(f"Driver list ({e.attempted_count}) exceeds cap ({e.max_allowed})")
except PoolFanOutError as e:
    # Custody-service rolled back any partially-added pool entries. Retry
    # the entire POST — the (VIN, origin) marker has been cleared.
    print(
        f"Pool fan-out failed after {e.succeeded_driver_count} drivers; "
        f"rollback {'incomplete' if e.rollback_failures else 'complete'}"
    )

get()

Retrieve a specific authorization by ID.

authorization_idstrrequired

Authorization ID (e.g., "AUTH-12345678")

Returns: Authorization object

Raises: AuthorizationNotFoundError if authorization doesn't exist

Example:

from vecu_custody import CustodyClient
from vecu_custody.exceptions import AuthorizationNotFoundError

client = CustodyClient.sandbox(token="your-jwt-token")

try:
    auth = client.authorizations.get("AUTH-12345678")
    print(f"VIN: {auth.vin}")
    print(f"Status: {auth.status}")
    print(f"Origin: {auth.origin}")
    print(f"Destination: {auth.destination}")
except AuthorizationNotFoundError:
    print("Authorization not found")

list()

List authorizations with optional filtering and pagination.

vinstr

Filter by Vehicle Identification Number

statusstr

Filter by status (e.g., "PENDING", "RELEASED", "CANCELLED")

limitint

Number of items per page (default: 50, max: 100)

next_tokenstr

Pagination cursor from previous response

auto_paginatebool

If True, returns iterator; if False, returns single page. Default: True

Returns: Iterator of Authorization objects (if auto_paginate=True) or AuthorizationListResponse (if auto_paginate=False)

Example:

from vecu_custody import CustodyClient

client = CustodyClient.sandbox(token="your-jwt-token")

# Auto-paginate through all authorizations (returns iterator)
for auth in client.authorizations.list(vin="9HGBH41JXMN999999"):
    print(f"{auth.authorization_id}: {auth.status}")

# Manual pagination (returns single page)
page = client.authorizations.list(
    vin="9HGBH41JXMN999999",
    limit=20,
    auto_paginate=False
)
print(f"Total count: {page.total_count}")
print(f"Page size: {len(page.authorizations)}")

if page.next_token:
    next_page = client.authorizations.list(
        vin="9HGBH41JXMN999999",
        next_token=page.next_token,
        auto_paginate=False
    )

cancel()

Cancel an authorization.

authorization_idstrrequired

Authorization ID (e.g., "AUTH-12345678")

Returns: Authorization object with status="CANCELLED"

Raises: AuthorizationNotFoundError if authorization doesn't exist, ValidationError if authorization cannot be cancelled

Example:

from vecu_custody import CustodyClient

client = CustodyClient.sandbox(token="your-jwt-token")

# Cancel an authorization
cancelled = client.authorizations.cancel("AUTH-12345678")
print(f"Status: {cancelled.status}")  # "CANCELLED"

update_drivers()

Add or remove drivers on an existing multi-driver authorization (ADR-042 §Decision §6). Available only when the authorization was created under multi_driver mode and the calling client owns the authorization. ETag-guarded for optimistic concurrency, idempotent on add/remove operations, and atomic across pool fan-out side effects.

authorization_idstrrequired

Authorization ID (canonical UUID, e.g., "123e4567-e89b-12d3-a456-426614174000").

versionintrequired

Current version of the authorization (the value last returned by create(), get(), update_drivers(), or assign_to_driver()). The SDK serializes this int as the quoted ETag form on the wire (e.g., If-Match: "3") per RFC 7232 — callers bypassing the SDK should match this convention. A stale version returns 412 Precondition Failed with the current version in the error body.

addlist[AuthorizedDriver]

Drivers to add to the list. Idempotent — adding a driver already on the list is a no-op (no duplicate entry, no duplicate pool fan-out). Per-driver name is display-only and does NOT appear in event payloads (per AC-16).

removelist[str]

Person identity keys to remove. Idempotent — removing a driver not on the list is a no-op. Removed drivers' pool entries are cancelled (best-effort) — the remove succeeds even if the pool-cancel fails for a previously-issued credential.

actor_identifierstr

Caller-asserted forensic identifier for the internal user that initiated the modification. Stored verbatim on the authorization and emitted on the custody.authorization.modified event; not verified by VECU.

Returns: updated Authorization with the new version and the post-PATCH authorized_drivers list. Save the new version for the next call.

Raises:

  • MultiDriverModeRequiredError — 400, the authorization was created under single_driver mode. PATCH /drivers is unavailable on single-driver authorizations even from the legitimate owner.
  • NotAuthorizationOwnerError — 403, the caller's client_id does not match the authorization's client_id.
  • EmptyDriverListError — 400, the net effect of the PATCH would leave zero drivers on the authorization. NO state changes applied.
  • AuthorizationAlreadyAssignedError — 409, the authorization has already been assigned via assign_to_driver(). Modifying drivers on an ASSIGNED authorization is not permitted.
  • StaleETagError — 412, the supplied version does not match the current version. Re-fetch via get() and retry with the fresh version.
  • DriverLimitExceededError — 429, the proposed list (after applying add/remove) exceeds the per-client cap (default 25). NO state changes applied.

Example:

from vecu_custody import CustodyClient
from vecu_custody.models import AuthorizedDriver
from vecu_custody.exceptions import (
    AuthorizationAlreadyAssignedError,
    EmptyDriverListError,
    MultiDriverModeRequiredError,
    StaleETagError,
)

client = CustodyClient.sandbox(token="your-jwt-token")

try:
    # Add Dave, remove Alice — atomic single round-trip
    updated = client.authorizations.update_drivers(
        authorization_id="123e4567-e89b-12d3-a456-426614174000",
        version=3,  # ETag from prior create() / get() / update_drivers()
        add=[
            AuthorizedDriver(
                person_identity_key="vecu_dAvEpKxYz1234567",
                name="Dave Driver",
            ),
        ],
        remove=["vecu_gfTRAjYnn_y-8zj-aBc4dEf5"],  # Alice's person_identity_key
        actor_identifier="ops-user-42@client.example.com",
    )
    print(f"New version: {updated.version}")  # 4
    print(f"Drivers now: {len(updated.authorized_drivers)}")
except StaleETagError as e:
    # Concurrent PATCH bumped the version; refetch and retry
    fresh = client.authorizations.get(
        "123e4567-e89b-12d3-a456-426614174000"
    )
    print(f"Stale version; current is {fresh.version}")
except MultiDriverModeRequiredError:
    print("Authorization was not created under multi_driver mode")
except EmptyDriverListError:
    print("PATCH would leave no drivers on the authorization")
except AuthorizationAlreadyAssignedError:
    print("Cannot modify drivers after PUT /assign")

assign_to_driver()

Lock the authorization to a single chosen driver (ADR-042 §Decision §7). Transitions the authorization to ASSIGNED, cancels non-assigned pool entries, and revokes credentials issued for non-assigned drivers. Sync-blocking: by the time the call returns, all non-assigned pool entries are cancelled and all non-assigned credentials are revoked at the credential service.

Available in both modes

assign_to_driver() works on both single_driver and multi_driver authorizations. On a single-driver authorization the call is a no-op-shaped confirmation of the existing 1-driver list — useful when you want to lock down the authorization without modifying its drivers.

authorization_idstrrequired

Authorization ID (canonical UUID).

versionintrequired

Current version (sent as If-Match). Same semantics as update_drivers().

person_identity_keystrrequired

Person identity key of the chosen driver. MUST be on the authorization's authorized_drivers list — otherwise 400 DRIVER_NOT_ON_LIST. Use update_drivers() first if you need to add a new driver before assigning.

actor_identifierstr

Caller-asserted forensic identifier; same semantics as update_drivers(). Emitted on custody.authorization.assigned.

Returns: AssignAuthorizationResponse with assigned_driver_pik, new version, revoked_credential_ids (credentials revoked as part of the assign — empty pre-acceptance), and driver_authorization_mode.

Raises:

  • DriverNotOnListError — 400, the requested person_identity_key is not on the authorization's authorized_drivers list. Add the driver via update_drivers() first.
  • NotAuthorizationOwnerError — 403, caller does not own the authorization.
  • AuthorizationAlreadyAssignedToDifferentDriverError — 409, the authorization is already ASSIGNED to a different driver. Reassignment is not permitted; cancel and recreate to assign a different driver.
  • AuthorizationNotAssignableError — 409, the authorization is in a terminal status (RELEASED, EXPIRED, REVOKED, or CANCELLED).
  • StaleETagError — 412, version mismatch.
  • AssignPartialFailureError — 503 ASSIGN_PARTIAL_FAILURE, the authorization IS assigned but credential revocation completed only partially. The call is safe to retry with the bumped version (revocation is idempotent on the credential-service side). Body carries revoked_credential_ids (succeeded) and failed_revocations (each with credential_id + reason).

Idempotency: assigning the already-assigned driver to an already- ASSIGNED authorization returns 200 with the existing state and a fresh ETag — safe to retry.

Example:

from vecu_custody import CustodyClient
from vecu_custody.exceptions import (
    AssignPartialFailureError,
    AuthorizationAlreadyAssignedToDifferentDriverError,
    DriverNotOnListError,
    StaleETagError,
)

client = CustodyClient.sandbox(token="your-jwt-token")

try:
    response = client.authorizations.assign_to_driver(
        authorization_id="123e4567-e89b-12d3-a456-426614174000",
        version=4,
        person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",  # Alice
        actor_identifier="ops-user-42@client.example.com",
    )
    print(f"Status: {response.status}")  # ASSIGNED
    print(f"Assigned driver: {response.assigned_driver_pik}")
    print(f"Revoked credentials: {response.revoked_credential_ids}")
    print(f"New version: {response.version}")
except DriverNotOnListError as e:
    print(
        f"Driver {e.requested_person_identity_key} is not on the list. "
        "Use update_drivers() to add first."
    )
except AuthorizationAlreadyAssignedToDifferentDriverError:
    print("Authorization already ASSIGNED to a different driver — cancel-and-recreate to change.")
except AssignPartialFailureError as e:
    # Authorization IS assigned; credential-revoke partially failed.
    # Safe to retry with the bumped version.
    print(
        f"Partial revocation: {len(e.revoked_credential_ids)} ok, "
        f"{len(e.failed_revocations)} failed"
    )
    # Retry with the bumped version
    response = client.authorizations.assign_to_driver(
        authorization_id="123e4567-e89b-12d3-a456-426614174000",
        version=e.current_version,
        person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",
    )

add_or_create_driver()

SDK-side convenience wrapper (ADR-042 §Decision §10). Tries create() first; on a 409 DUPLICATE_AUTHORIZATION for the same (VIN, origin), falls back to fetching the existing authorization via list() (auto-scoped to the caller's client_id) and update_drivers() to add the driver. Returns a unified result indicating which path was taken.

This method exists so a per-driver call shape (one driver per request) can map onto the strict-uniqueness server design without the client having to write the POST→409→GET→PATCH dance manually.

vinstrrequired

17-character VIN.

originstrrequired

Origin location ID — used for both the initial POST attempt and the client-side filter when listing existing authorizations on fallback.

destinationstrrequired

Destination location ID — only used on the create path.

driverAuthorizedDriverrequired

The single driver to add or create the authorization with.

authorized_bystrrequired

System or user initiating the request.

make_modelstrrequired

Vehicle make/model (used on the create path).

actor_identifierstr

Caller-asserted forensic identifier. Forwarded to either create() or update_drivers().

max_retriesint

Bound on ETag-mismatch retry attempts on the PATCH fallback path (default 3). Exceeding raises RetryExhaustedError.

Returns: AddOrCreateDriverResult with:

  • authorization — the resulting Authorization.
  • createdTrue if the POST path was taken (new authorization); False if the PATCH fallback was taken.
  • addedTrue if update_drivers() actually added the driver; False if the driver was already on the existing list (idempotent no-op).

Raises: any of the typed exceptions raised by create() or update_drivers(). Only the specific 409 DUPLICATE_AUTHORIZATION case triggers the PATCH fallback — other 409 reasons (e.g., AUTHORIZATION_ALREADY_ASSIGNED) bubble up unchanged. ETag-mismatch retries are bounded; exhaustion raises RetryExhaustedError.

Example:

from vecu_custody import CustodyClient
from vecu_custody.models import AuthorizedDriver
from vecu_custody.exceptions import RetryExhaustedError

client = CustodyClient.sandbox(token="your-jwt-token")

try:
    result = client.authorizations.add_or_create_driver(
        vin="9HGBH41JXMN999999",
        origin="LOC-AUCTION-MANHEIM-ATLANTA",
        destination="LOC-DEALERSHIP-CARMAX-ORLANDO",
        driver=AuthorizedDriver(
            person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",
            name="Alice Driver",
        ),
        authorized_by="dispatch-system",
        make_model="Honda Accord 2023",
        actor_identifier="ops-user-42@client.example.com",
    )

    if result.created:
        print(f"Created new authorization {result.authorization.authorization_id}")
    elif result.added:
        print(f"Added driver to existing authorization {result.authorization.authorization_id}")
    else:
        print(f"Driver was already on the list (no-op)")
except RetryExhaustedError:
    # ETag conflicts persisted past the retry budget — another caller
    # is mutating the same authorization concurrently
    print("Concurrent modifications exhausted retry budget; try again later")

Response Object

Authorization

authorization_idstr

Unique identifier for the authorization (e.g., "AUTH-12345678")

vinstr

17-character Vehicle Identification Number

originstr

Origin location ID

destinationstr

Destination location ID

person_identity_keystr

Person identity from VECU IDV SDK

roleAuthorizationRole

Authorization role: OWNER, ADMIN, or DRIVER

statusAuthorizationStatus

Current status: PENDING, ASSIGNED, RELEASED, REVOKED, EXPIRED, CANCELLED. ASSIGNED (ADR-042) is a refinement of PENDING set by assign_to_driver() — it is NOT a terminal status; the existing PENDING → RELEASED|EXPIRED|REVOKED|CANCELLED transitions still apply.

buyer_identity_keystr

Buyer identity key (if provided)

transport_order_idstr

Transport order ID (if provided)

permitted_actionslist[str]

List of permitted actions

created_atstr

ISO 8601 timestamp when authorization was created

expires_atstr

ISO 8601 timestamp when authorization expires

authorized_driverslist[AuthorizedDriver]

Multi-driver authorizations (ADR-042) — the current list of drivers eligible to take custody. Empty list / absent on single-driver authorizations. Each entry exposes person_identity_key (the mapped HMAC form per ADR-043) and name (display-only — does NOT appear in event payloads).

versionint

Multi-driver authorizations (ADR-042) — monotonic version number incremented on every update_drivers() and assign_to_driver() call. Use as the version argument on subsequent modification calls for ETag-style optimistic concurrency. Single-driver authorizations return version 1 and never increment (no PATCH /assign verbs apply).

driver_authorization_modestr

Multi-driver authorizations (ADR-042) — single_driver or multi_driver. Stamped at authorization-creation time and frozen on the record; the calling client's live config does not retroactively change the mode of existing authorizations.

actor_identifierstr

Caller-asserted forensic identifier (ADR-042 §Decision §9). Reflects the most recent value the caller supplied on create(), update_drivers(), or assign_to_driver() — preserved across omitted-actor calls. Not verified.

assigned_driver_pikstr

Multi-driver authorizations only — the chosen driver's raw person_identity_key once assign_to_driver() has been called. Absent when the authorization is still PENDING (no assign yet). Server-side identity- mapping per ADR-043 happens in the event pipeline, not the response — the response surfaces the raw value the caller supplied on assign_to_driver().

AssignAuthorizationResponse

Returned by assign_to_driver(). Extends the standard authorization shape with revocation telemetry from the sync-blocking AC-12 contract.

authorization_idstr

Authorization ID that was assigned.

vinstr

Vehicle Identification Number.

statusAuthorizationStatus

ASSIGNED on success.

assigned_driver_pikstr

Raw person_identity_key of the chosen driver (server-side identity- mapping per ADR-043 happens in the event pipeline, not the response).

versionint

New monotonic version after the assign committed.

driver_authorization_modestr

Stamped mode (single_driver or multi_driver).

actor_identifierstr

Caller-asserted forensic identifier (preserved if not supplied on this call).

revoked_credential_idslist[str]

Credential IDs revoked at the credential service as part of the assign. Empty list when no non-assigned drivers had issued credentials (pre-acceptance assigns). On 503 ASSIGN_PARTIAL_FAILURE, this list contains only the credentials that succeeded — the failures live on AssignPartialFailureError.failed_revocations.

Common Use Cases

Auction to Dealership Transfer

from vecu_custody import CustodyClient

client = CustodyClient.sandbox(token="your-jwt-token")

# Create authorization for auction to dealership transfer
auth = client.authorizations.create(
    vin="9HGBH41JXMN999999",
    origin="LOC-AUCTION-MANHEIM-ATLANTA",
    destination="LOC-DEALERSHIP-CARMAX-ORLANDO",
    person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",
    authorized_by="system-auction-integration",
    make_model="Honda Accord 2023",
    transport_order_id="TO-2024-001"
)

print(f"Authorization ID: {auth.authorization_id}")
print(f"Valid until: {auth.expires_at}")

Check Authorization Status

# Get authorization details
auth = client.authorizations.get("AUTH-12345678")

if auth.status == "RELEASED":
    print("Vehicle has been released")
elif auth.status == "PENDING":
    print("Authorization pending — waiting for releasability")
elif auth.status == "REVOKED":
    print("Authorization has been revoked")
elif auth.status == "EXPIRED":
    print("Authorization has expired")

List Active Authorizations for VIN

# Get all authorizations for a specific vehicle
for auth in client.authorizations.list(vin="9HGBH41JXMN999999"):
    print(f"{auth.authorization_id}: {auth.status}")
    print(f"  Route: {auth.origin} → {auth.destination}")
    print(f"  Expires: {auth.expires_at}")

Error Handling

ADR-042 disclosure-guard caveat

Per ADR-042, DuplicateAuthorizationError.existing_authorization_id is suppressed on cross-tenant 409s — the disclosure guard reduces to same_client only. Always guard with getattr(e, "existing_authorization_id", None) rather than direct attribute access; treat a missing/None value as "conflict exists but the existing record is not yours to see."

from vecu_custody import CustodyClient
from vecu_custody.exceptions import (
    AuthorizationNotFoundError,
    DuplicateAuthorizationError,
    ValidationError,
)

client = CustodyClient.sandbox(token="your-jwt-token")

try:
    auth = client.authorizations.create(
        vin="9HGBH41JXMN999999",
        origin="LOC-AUCTION-MANHEIM-ATLANTA",
        destination="LOC-DEALERSHIP-CARMAX-ORLANDO",
        person_identity_key="vecu_gfTRAjYnn_y-8zj-aBc4dEf5",
        authorized_by="system-integration",
        make_model="Honda Accord 2023",
    )
except DuplicateAuthorizationError as e:
    # ADR-042: existing_authorization_id is suppressed on cross-tenant
    # 409s (disclosure guard reduces to same_client). Guard for None.
    existing_id = getattr(e, "existing_authorization_id", None)
    if existing_id is not None:
        print(f"Authorization already exists: {existing_id}")
    else:
        print("(VIN, origin) already in use by a different tenant")
except ValidationError as e:
    print(f"Invalid parameters: {e.message}")

Multi-Driver Exception Hierarchy (ADR-042)

Multi-driver operations introduce additional typed exceptions. All inherit from CustodyError and the appropriate intermediate base class (e.g., MultiDriverModeRequiredError is a subclass of InvalidConfigurationError, mirroring the existing InvalidOriginAddressError precedent).

ExceptionHTTPServer error codeRaised when
MultiDriverModeRequiredError400MULTI_DRIVER_MODE_REQUIREDCaller's client is in single_driver mode but the request used a multi-driver endpoint or supplied authorized_drivers.
EmptyDriverListError400EMPTY_DRIVER_LISTupdate_drivers() net effect would leave zero drivers.
DriverNotOnListError400DRIVER_NOT_ON_LISTassign_to_driver() requested a driver not on the authorization's authorized_drivers list.
NotAuthorizationOwnerError403NOT_AUTHORIZATION_OWNERCaller's client_id does not own the authorization (cross-tenant attempt).
AuthorizationAlreadyAssignedError409AUTHORIZATION_ALREADY_ASSIGNEDupdate_drivers() against an ASSIGNED authorization.
AuthorizationAlreadyAssignedToDifferentDriverError409AUTHORIZATION_ALREADY_ASSIGNED_TO_DIFFERENT_DRIVERassign_to_driver() requested a different driver on an already-ASSIGNED authorization.
AuthorizationNotAssignableError409AUTHORIZATION_NOT_ASSIGNABLEassign_to_driver() against a terminal-status authorization.
StaleETagError412STALE_ETAGversion argument does not match the authorization's current version. Body carries current_version for retry.
DriverLimitExceededError429DRIVER_LIMIT_EXCEEDEDMulti-driver POST or update_drivers() add would exceed per-client driver cap.
PoolFanOutError503POOL_FANOUT_FAILUREMulti-driver POST pool fan-out failed and was rolled back. Retry the entire POST.
AssignPartialFailureError503ASSIGN_PARTIAL_FAILUREassign_to_driver() succeeded at the authorization level but credential revocation completed only partially.
RetryExhaustedErrorn/a(SDK-side)add_or_create_driver() exhausted its ETag-mismatch retry budget. Caller should back off and retry.
from vecu_custody.exceptions import (
    AssignPartialFailureError,
    AuthorizationAlreadyAssignedError,
    AuthorizationAlreadyAssignedToDifferentDriverError,
    AuthorizationNotAssignableError,
    DriverLimitExceededError,
    DriverNotOnListError,
    EmptyDriverListError,
    InvalidConfigurationError,  # Parent of MultiDriverModeRequiredError
    MultiDriverModeRequiredError,
    NotAuthorizationOwnerError,
    PoolFanOutError,
    RetryExhaustedError,
    StaleETagError,
)

# Catch the parent class to handle the whole config-error family at once
try:
    client.authorizations.update_drivers(...)
except InvalidConfigurationError as e:
    # Handles MultiDriverModeRequiredError + any future config errors
    print(f"Configuration issue: {e}")

Next Steps