Custody Permits

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

SDK-Only Access

To ensure your integration remains stable and supported, always use the official SDKs. The underlying API endpoints are internal implementation details that may change in purpose, functionality, or be removed at any time without notice. Direct API access is not supported. Learn more →

The SDK methods documented here are your supported interface. Versioning is managed through SDK upgrades.

Overview

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

  • VIN: The vehicle being authorized
  • Route: Origin and destination locations (OD pair)
  • Person Identity: Who can take custody (from VECU IDV SDK)
  • Transport Order: Optional external shipment/order reference
  • 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 CreateAuthorizationAsync() accepting an AuthorizedDrivers list, UpdateAuthorizationDriversAsync() to add or remove drivers, and AssignAuthorizationToDriverAsync() to lock the authorization to one driver. See the Multi-Driver Integration Guide for end-to-end flows.

Methods

CreateAuthorizationAsync()

Create a new custody authorization for a vehicle.

requestCreateAuthorizationRequestrequired

Authorization creation request containing VIN, origin/destination, and identity information

cancellationTokenCancellationToken

Optional cancellation token for the async operation

CreateAuthorizationRequest Properties

Vinstringrequired

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

Originstringrequired

Origin facility ID

Destinationstringrequired

Destination facility ID

PersonIdentityKeystringrequired

Person identity credential from VECU IDV SDK

MakeModelstringrequired

Vehicle make and model (e.g., "Honda Accord", "Ford F-150")

AuthorizedBystringrequired

System or user ID initiating the request

TransportOrderIdstring?

External transport order or shipment ID (optional)

ValidUntilDateTimeOffset?

Authorization expiration timestamp in ISO 8601 format (default: 24 hours from creation)

RoleAuthorizationRole?

Role of the authorized party: OWNER, ADMIN, or DRIVER

PermittedActionsList<string>?

Optional list of permitted actions for this authorization

BuyerIdentityKeystring?

Optional buyer identity key

OrganizationIdstring?

Optional organization identifier (2–128 chars, pattern ^[A-Za-z0-9][A-Za-z0-9_\-.]*[A-Za-z0-9]$). Per ADR-042, OrganizationId is caller-asserted forensic metadata only — accepted, stored verbatim on the authorization record, and echoed on events and 409 envelopes for downstream forensic correlation. It does NOT participate in marker uniqueness or the 409 disclosure guard. Strict (VIN, geohash) uniqueness is enforced unconditionally; ADR-040's VinOriginSameCompanyAllowed scope was superseded by ADR-042 and removed.

AuthorizedDriversList<AuthorizedDriver>?

Multi-driver mode (ADR-042) — list of drivers eligible to take custody; one will be locked in via AssignAuthorizationToDriverAsync() later. Mutually exclusive with PersonIdentityKey — 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).

ActorIdentifierstring?

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: AuthorizationResponse with authorization details and status

Exceptions:

  • ValidationException - Invalid request parameters (400, 422)
  • InvalidOriginAddressException - Origin address rejected by the geocoder (400, ErrorCode=INVALID_ORIGIN_ADDRESS)
  • AuthException - Authentication failure (401)
  • DuplicateAuthorizationException - Authorization already exists for (VIN, canonicalized origin) (409). Per ADR-042 the disclosure guard is same_client only — cross-tenant collisions return the conflict but omit ExistingAuthorization and the Location header. ExistingAuthorization is nullable — see Error Handling.
  • VehicleNotReleasableException - Vehicle cannot be released due to business rules (409)
  • RateLimitException - Rate limit exceeded (429)
  • PoolCapacityExceededException - Authorization pool at capacity for this VIN under the effective uniqueness scope (429, ErrorCode=POOL_CAPACITY_EXCEEDED)
  • DriverLimitExceededException - Multi-driver POST exceeded per-client cap (429, ErrorCode=DRIVER_LIMIT_EXCEEDED). Body carries AttemptedCount + MaxAllowed. NO state changes applied (atomic pre-write rejection).
  • MultiDriverModeRequiredException - Caller's client is in single_driver mode but AuthorizedDrivers was supplied (400, ErrorCode=MULTI_DRIVER_MODE_REQUIRED). Subclass of InvalidConfigurationException — catch the parent class to handle the whole config-error family.
  • PoolFanOutException - Multi-driver POST pool fan-out failed and was rolled back (503, ErrorCode=POOL_FANOUT_FAILURE). Caller can retry the entire POST. Body carries SucceededDriverCount, RolledBackEntries, and RollbackFailures (composite IDs left orphaned at auth-pool — ops attention required).
  • GeocodingServiceUnavailableException - Upstream geocoder unavailable; SDK has exhausted its retry budget (503, ErrorCode=GEOCODING_SERVICE_UNAVAILABLE)
  • NetworkException - Network or server errors (5xx)

Example:

using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Exceptions;

public class AuthorizationService
{
    private readonly ICustodyServiceClient _client;

    public AuthorizationService(ICustodyServiceClient client)
    {
        _client = client;
    }

    public async Task<string> CreateAuthorizationAsync()
    {
        var request = new CreateAuthorizationRequest
        {
            Vin = "1HGBH41JXMN109186",
            Origin = "1 Auction Blvd, Bordentown, NJ 08505",
            Destination = "200 Motor Ave, Columbus, OH 43230",
            PersonIdentityKey = "DL-293-847-561",
            MakeModel = "Honda Accord",
            AuthorizedBy = "CENTRAL_DISPATCH",
            TransportOrderId = "TRO-2024-00472",
            ValidUntil = DateTimeOffset.UtcNow.AddHours(48),
            Role = AuthorizationRole.DRIVER
        };

        try
        {
            var response = await _client.CreateAuthorizationAsync(request);

            Console.WriteLine($"Authorization created: {response.AuthorizationId}");
            Console.WriteLine($"Status: {response.Status}");
            Console.WriteLine($"Created at: {response.CreatedAt}");
            Console.WriteLine($"Expires at: {response.ExpiresAt}");
            Console.WriteLine($"Effective scope: {response.UniquenessScope}");
            Console.WriteLine($"Origin (formatted): {response.OriginFormattedAddress}");
            Console.WriteLine($"Origin geohash: {response.OriginGeohash}");
            Console.WriteLine($"Location header: {response.LocationUri}");

            return response.AuthorizationId;
        }
        catch (DuplicateAuthorizationException ex)
        {
            // ExistingAuthorization is nullable as of v3.2.0 — gate on the
            // null check ALONE. Per ADR-040 §7.1, do NOT infer
            // disclosure-guard denial from a null value (it can also be a
            // transient enrichment failure).
            if (ex.ExistingAuthorization is not null)
            {
                Console.WriteLine($"Existing authorization: {ex.ExistingAuthorization.AuthorizationId}");
            }
            else
            {
                Console.WriteLine($"Conflict on VIN/origin; existing record not surfaced. EffectiveScope={ex.EffectiveScope}");
            }
            throw;
        }
        catch (VehicleNotReleasableException ex)
        {
            Console.WriteLine($"Vehicle not releasable: {ex.Message}");
            throw;
        }
    }
}

GetAuthorizationAsync()

Retrieve detailed information about an authorization by ID.

authorizationIdstringrequired

Unique authorization identifier (e.g., "AUTH-xyz789")

cancellationTokenCancellationToken

Optional cancellation token for the async operation

Returns: AuthorizationDetails with full authorization information

Exceptions:

  • ArgumentException - Authorization ID is null or whitespace
  • AuthException - Authentication failure (401)
  • AuthorizationNotFoundException - Authorization not found (404)
  • NetworkException - Network or server errors (5xx)

Example:

using CoxAuto.Vecu.CustodySdk.Exceptions;

public async Task CheckAuthorizationStatus(string authorizationId)
{
    try
    {
        var details = await _client.GetAuthorizationAsync(authorizationId);

        Console.WriteLine($"Authorization: {details.AuthorizationId}");
        Console.WriteLine($"VIN: {details.Vin}");
        Console.WriteLine($"Status: {details.Status}");
        Console.WriteLine($"Origin: {details.Origin}");
        Console.WriteLine($"Destination: {details.Destination}");
        Console.WriteLine($"Role: {details.Role}");
        Console.WriteLine($"Expires at: {details.ExpiresAt}");

        if (details.CancelledAt != null)
        {
            Console.WriteLine($"Cancelled at: {details.CancelledAt}");
            Console.WriteLine($"Cancelled by: {details.CancelledBy}");
            Console.WriteLine($"Reason: {details.CancellationReason}");
        }
    }
    catch (AuthorizationNotFoundException ex)
    {
        Console.WriteLine($"Authorization not found: {ex.Message}");
    }
}

CancelAuthorizationAsync()

Cancel an existing authorization.

authorizationIdstringrequired

Unique authorization identifier to cancel

requestCancelAuthorizationRequestrequired

Cancellation request with reason and optional notes

cancellationTokenCancellationToken

Optional cancellation token for the async operation

CancelAuthorizationRequest Properties

CancellationReasonstringrequired

Free-form reason for cancellation (minimum 10 characters)

Returns: CancelAuthorizationResponse with cancellation details

Exceptions:

  • ArgumentException - Authorization ID is null or whitespace
  • ArgumentNullException - Request is null
  • AuthException - Authentication failure (401)
  • AuthorizationNotFoundException - Authorization not found (404)
  • ValidationException - Authorization cannot be cancelled (422)
  • NetworkException - Network or server errors (5xx)

Example:

using CoxAuto.Vecu.CustodySdk.Models.Requests;

public async Task CancelAuthorization(string authorizationId)
{
    var request = new CancelAuthorizationRequest
    {
        CancellationReason = "Driver reassigned to different route due to scheduling conflict"
    };

    try
    {
        var response = await _client.CancelAuthorizationAsync(
            authorizationId,
            request);

        Console.WriteLine($"Authorization cancelled: {response.AuthorizationId}");
        Console.WriteLine($"VIN: {response.Vin}");
        Console.WriteLine($"Cancelled at: {response.CancelledAt}");
        Console.WriteLine($"Cancelled by: {response.CancelledBy}");
        Console.WriteLine($"Status: {response.Status}");
        Console.WriteLine($"Reason: {response.CancellationReason}");
    }
    catch (ValidationException ex)
    {
        Console.WriteLine($"Cannot cancel: {ex.Message}");
        // Authorization might already be cancelled, expired, or in use
    }
}

WaitForAuthorizationAsync()

Wait for an authorization to reach a terminal status by polling.

authorizationIdstringrequired

Unique authorization identifier to wait for

optionsWaitOptions?

Optional wait configuration (polling interval, timeout, custom predicate)

cancellationTokenCancellationToken

Optional cancellation token for the async operation

WaitOptions Properties

PollingIntervalSecondsint

Polling interval in seconds (1-60, default: 5)

TimeoutSecondsint

Maximum wait timeout in seconds (1-3600, default: 300)

CustomPredicateFunc<AuthorizationDetails, bool>?

Optional custom completion condition

Returns: AuthorizationDetails when terminal status or custom condition is reached

Terminal Statuses: RELEASED, CANCELLED, EXPIRED, REVOKED

Exceptions:

  • ArgumentException - Authorization ID is null or whitespace
  • AuthException - Authentication failure (401)
  • AuthorizationNotFoundException - Authorization not found (404)
  • TimeoutException - Wait timeout exceeded before reaching terminal status
  • OperationCanceledException - Operation cancelled via cancellation token
  • NetworkException - Network or server errors (5xx)

Example:

using CoxAuto.Vecu.CustodySdk.Client;

public async Task WaitForAuthorization(string authorizationId)
{
    try
    {
        // Wait with default options (5s interval, 5min timeout)
        var details = await _client.WaitForAuthorizationAsync(authorizationId);

        Console.WriteLine($"Authorization reached terminal status: {details.Status}");
        Console.WriteLine($"Expires at: {details.ExpiresAt}");
    }
    catch (TimeoutException)
    {
        Console.WriteLine("Authorization did not reach terminal status within timeout");
    }
}

public async Task WaitWithCustomOptions(string authorizationId)
{
    // Custom wait options
    var options = new WaitOptions
    {
        PollingIntervalSeconds = 10,  // Check every 10 seconds
        TimeoutSeconds = 600,          // Wait up to 10 minutes
        CustomPredicate = auth => auth.Status == AuthorizationStatus.RELEASED
    };

    var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));

    try
    {
        var details = await _client.WaitForAuthorizationAsync(
            authorizationId,
            options,
            cts.Token);

        Console.WriteLine($"Authorization released: {details.AuthorizationId}");
    }
    catch (TimeoutException)
    {
        Console.WriteLine("Authorization was not released within timeout");
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Wait operation was cancelled");
    }
}

UpdateAuthorizationDriversAsync()

Add or remove drivers on a 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.

authorizationIdstringrequired

Authorization ID (canonical UUID).

requestUpdateDriversRequestrequired

Add list, remove list, current Version (sent as If-Match), and optional ActorIdentifier.

cancellationTokenCancellationToken

Optional cancellation token for the async operation.

UpdateDriversRequest Properties

Versionintrequired

Current monotonic version of the authorization (the value last returned by CreateAuthorizationAsync, GetAuthorizationAsync, or a prior UpdateAuthorizationDriversAsync / AssignAuthorizationToDriverAsync call). 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; the SDK surfaces this as StaleETagException with CurrentVersion.

AddList<AuthorizedDriver>?

Drivers to add to the list. Idempotent — adding a driver already on the list is a no-op. Per-driver Name is display-only and does NOT appear in event payloads (per ADR-042 AC-16).

RemoveList<string>?

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.

ActorIdentifierstring?

Caller-asserted forensic identifier; emitted on custody.authorization.modified. Not verified.

Returns: UpdateDriversResponse with the new Version and the post-PATCH AuthorizedDrivers list.

Exceptions:

  • MultiDriverModeRequiredException - 400, the authorization was created under single_driver mode (subclass of InvalidConfigurationException).
  • NotAuthorizationOwnerException - 403, caller's client_id does not match the authorization's client_id.
  • EmptyDriverListException - 400, net effect of the PATCH would leave zero drivers. NO state changes.
  • AuthorizationAlreadyAssignedException - 409, authorization is already ASSIGNED — driver-list modifications no longer permitted.
  • StaleETagException - 412, version mismatch. Refetch via GetAuthorizationAsync and retry.
  • DriverLimitExceededException - 429, proposed list (after applying add/remove) exceeds per-client cap. NO state changes.
  • AuthException - 401, invalid or missing JWT.
  • NetworkException - 5xx network or server errors.

Example:

using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Models;
using CoxAuto.Vecu.CustodySdk.Exceptions;

public async Task UpdateDriversExample(string authorizationId, int currentVersion)
{
    var request = new UpdateDriversRequest
    {
        Version = currentVersion,  // ETag from prior fetch
        Add = new List<AuthorizedDriver>
        {
            new()
            {
                PersonIdentityKey = "vecu_dAvEpKxYz1234567",
                Name = "Dave Driver",
            },
        },
        Remove = new List<string> { "vecu_gfTRAjYnn_y-8zj-aBc4dEf5" },  // Alice's personIdentityKey
        ActorIdentifier = "ops-user-42@client.example.com",
    };

    try
    {
        var response = await _client.UpdateAuthorizationDriversAsync(
            authorizationId, request);

        Console.WriteLine($"New version: {response.Version}");
        Console.WriteLine($"Drivers now: {response.AuthorizedDrivers.Count}");
    }
    catch (StaleETagException ex)
    {
        // Concurrent PATCH bumped the version — refetch and retry
        var fresh = await _client.GetAuthorizationAsync(authorizationId);
        Console.WriteLine($"Stale version; current is {fresh.Version}");
    }
    catch (MultiDriverModeRequiredException)
    {
        Console.WriteLine("Authorization was not created under multi_driver mode");
    }
    catch (EmptyDriverListException)
    {
        Console.WriteLine("PATCH would leave no drivers");
    }
    catch (AuthorizationAlreadyAssignedException)
    {
        Console.WriteLine("Cannot modify drivers after AssignAuthorizationToDriverAsync");
    }
}

AssignAuthorizationToDriverAsync()

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 credentials are revoked at the credential service.

Available in both modes

AssignAuthorizationToDriverAsync 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.

authorizationIdstringrequired

Authorization ID (canonical UUID).

requestAssignAuthorizationRequestrequired

Chosen PersonIdentityKey, current Version, optional ActorIdentifier.

cancellationTokenCancellationToken

Optional cancellation token.

AssignAuthorizationRequest Properties

Versionintrequired

Current monotonic version. Same ETag semantics as UpdateAuthorizationDriversAsync.

PersonIdentityKeystringrequired

Mapped person identity key of the chosen driver. MUST be on the authorization's AuthorizedDrivers list — otherwise 400 DRIVER_NOT_ON_LIST.

ActorIdentifierstring?

Caller-asserted forensic identifier; emitted on custody.authorization.assigned. Not verified.

Returns: AssignAuthorizationResponse with AssignedDriverPik, new Version, RevokedCredentialIds (credentials revoked as part of the assign — empty pre-acceptance), and DriverAuthorizationMode.

Exceptions:

  • DriverNotOnListException - 400, requested PersonIdentityKey is not on the authorization's AuthorizedDrivers list. Use UpdateAuthorizationDriversAsync first to add.
  • NotAuthorizationOwnerException - 403, caller does not own the authorization.
  • AuthorizationAlreadyAssignedToDifferentDriverException - 409, the authorization is already ASSIGNED to a different driver. Cancel-and-recreate is the only path to change the assigned driver.
  • AuthorizationNotAssignableException - 409, the authorization is in a terminal status (RELEASED, EXPIRED, REVOKED, CANCELLED).
  • StaleETagException - 412, version mismatch.
  • AssignPartialFailureException - 503 ASSIGN_PARTIAL_FAILURE. The authorization IS assigned; credential revocation completed only partially. Safe to retry with the bumped version (revocation is idempotent on the credential-service side). Body carries RevokedCredentialIds (succeeded) and FailedRevocations (each with CredentialId + Reason).

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

Example:

using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Exceptions;

public async Task AssignDriver(string authorizationId, int currentVersion)
{
    var request = new AssignAuthorizationRequest
    {
        Version = currentVersion,
        PersonIdentityKey = "vecu_gfTRAjYnn_y-8zj-aBc4dEf5",  // Alice
        ActorIdentifier = "ops-user-42@client.example.com",
    };

    try
    {
        var response = await _client.AssignAuthorizationToDriverAsync(
            authorizationId, request);

        Console.WriteLine($"Status: {response.Status}");  // ASSIGNED
        Console.WriteLine($"Assigned driver: {response.AssignedDriverPik}");
        Console.WriteLine(
            $"Revoked credentials: {string.Join(", ", response.RevokedCredentialIds)}");
    }
    catch (DriverNotOnListException ex)
    {
        Console.WriteLine(
            $"Driver {ex.RequestedPersonIdentityKey} not on list; " +
            "call UpdateAuthorizationDriversAsync first.");
    }
    catch (AuthorizationAlreadyAssignedToDifferentDriverException)
    {
        Console.WriteLine("Already ASSIGNED to a different driver.");
    }
    catch (AssignPartialFailureException ex)
    {
        // Authorization IS assigned; credential-revoke partially failed.
        // Safe to retry with the bumped version.
        Console.WriteLine(
            $"Partial revocation: {ex.RevokedCredentialIds.Count} ok, " +
            $"{ex.FailedRevocations.Count} failed");
        var retryRequest = request with { Version = ex.CurrentVersion };
        await _client.AssignAuthorizationToDriverAsync(authorizationId, retryRequest);
    }
}

AddOrCreateDriverAsync()

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

requestAddOrCreateDriverRequestrequired

VIN, origin, destination, the single driver to add or create with, AuthorizedBy, MakeModel, optional ActorIdentifier, optional MaxRetries (default 3 for ETag-mismatch retries on the PATCH fallback path).

cancellationTokenCancellationToken

Optional cancellation token.

Returns: AddOrCreateDriverResult with:

  • Authorization — the resulting authorization.
  • Createdtrue if the POST path was taken; false if the PATCH fallback was taken.
  • Addedtrue if UpdateAuthorizationDriversAsync actually added the driver; false if the driver was already on the existing list (idempotent no-op).

Exceptions: any exception raised by CreateAuthorizationAsync or UpdateAuthorizationDriversAsync. 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 RetryExhaustedException.

Example:

using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.Models;
using CoxAuto.Vecu.CustodySdk.Exceptions;

public async Task AddOrCreate(string vin, string driverPik)
{
    var request = new AddOrCreateDriverRequest
    {
        Vin = vin,
        Origin = "1 Auction Blvd, Bordentown, NJ 08505",
        Destination = "200 Motor Ave, Columbus, OH 43230",
        Driver = new AuthorizedDriver
        {
            PersonIdentityKey = driverPik,
            Name = "Alice Driver",
        },
        AuthorizedBy = "dispatch-system",
        MakeModel = "Honda Accord 2023",
        ActorIdentifier = "ops-user-42@client.example.com",
    };

    try
    {
        var result = await _client.AddOrCreateDriverAsync(request);

        if (result.Created)
        {
            Console.WriteLine(
                $"Created new authorization {result.Authorization.AuthorizationId}");
        }
        else if (result.Added)
        {
            Console.WriteLine(
                $"Added driver to existing authorization {result.Authorization.AuthorizationId}");
        }
        else
        {
            Console.WriteLine("Driver was already on the list (no-op)");
        }
    }
    catch (RetryExhaustedException)
    {
        // ETag conflicts persisted past the retry budget — concurrent
        // modification by another caller
        Console.WriteLine("Concurrent modifications exhausted retry budget");
    }
}

Response Objects

AuthorizationResponse

Response after creating an authorization. Returned on 201 Created. On 409 Conflict, embedded as DuplicateAuthorizationException.ExistingAuthorization only when the disclosure guard passes (same client) AND service-side enrichment succeeded. Per ADR-042, the disclosure guard simplified to same_client only — cross-tenant collisions return the conflict but omit ExistingAuthorization and the Location header. The service still falls back to the minimal envelope on enrichment failure even when the guard would otherwise pass, so a null ExistingAuthorization does not by itself imply a cross-tenant conflict.

AuthorizationIdstring

Unique authorization identifier

Vinstring

Vehicle Identification Number

StatusAuthorizationStatus

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

Originstring

Origin facility ID as submitted on the request (raw, pre-canonicalization)

Destinationstring

Destination facility ID

CreatedAtDateTimeOffset

Authorization creation timestamp

ExpiresAtDateTimeOffset

Authorization expiration timestamp

UniquenessScopeUniquenessScope

Deprecated under ADR-042. Always returns VinOriginStrict for new authorizations — strict (VIN, geohash) uniqueness is the unconditional invariant. Pre-ADR-042 records may carry historical values (VinOriginSameCompanyAllowed) for forensic reasons. Will be removed from the response in a future SDK major version; do not branch on this value going forward.

OrganizationIdstring?

Echoed back if supplied on the request — caller-asserted forensic metadata only per ADR-042. Does NOT participate in marker uniqueness or 409 disclosure-guard semantics.

OriginGeohashstring?

Precision-7 geohash of the canonicalized origin address (≈152 m × 152 m cell) per ADR-041. The marker key the service uses for uniqueness is derived from this geohash, not from the raw Origin string. Nullable for migration-window and pre-ADR-041 terminal records that were backfilled without geohash data; populated on all new responses.

OriginFormattedAddressstring?

Amazon Location Service canonical address string for the geocoded origin. Surface this back to end users when echoing the authorization so they see the address as the system stored it. Nullable for the same migration-window reasons as OriginGeohash.

LocationUriUri?

Parsed value of the Location response header. Present on 201 Created. On 409 Conflict, present only when the disclosure guard passes (same client) AND service-side enrichment succeeded (per ADR-042 — the prior same-company disclosure clause was removed); null on cross-tenant conflicts and on enrichment fail-closed cases. Tracks the same nullability semantics as DuplicateAuthorizationException.ExistingAuthorization. When populated, points at GET /v1/authorizations/{id} for the authoritative authorization (newly created or pre-existing duplicate).

AuthorizedDriversList<AuthorizedDriver>?

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

Versionint?

Multi-driver authorizations (ADR-042) — monotonic version number incremented on every UpdateAuthorizationDriversAsync and AssignAuthorizationToDriverAsync call. Use as the Version argument on subsequent modification calls (sent as If-Match). Single-driver authorizations return version 1 and never increment.

DriverAuthorizationModestring?

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.

ActorIdentifierstring?

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

AssignedDriverPikstring?

Multi-driver authorizations only — the chosen driver's raw PersonIdentityKey once AssignAuthorizationToDriverAsync 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.

AuthorizationDetails

Detailed authorization information.

AuthorizationIdstring

Unique authorization identifier

StatusAuthorizationStatus

Current status (PENDING, ASSIGNED, RELEASED, REVOKED, EXPIRED, CANCELLED). ASSIGNED is a refinement of PENDING introduced by ADR-042 — not a terminal status.

Vinstring

Vehicle Identification Number

Originstring

Origin facility ID

Destinationstring

Destination facility ID

RoleAuthorizationRole

Role of the authorized party: OWNER, ADMIN, or DRIVER

CreatedAtDateTimeOffset

Creation timestamp

UpdatedAtDateTimeOffset

Last update timestamp

ExpiresAtDateTimeOffset

Expiration timestamp

PoolIdstring?

Authorization pool ID (if applicable)

AuthPoolAuthorizationIdstring?

Pool authorization ID (if applicable)

CancelledAtDateTimeOffset?

Cancellation timestamp (null if not cancelled)

CancelledBystring?

User or system that cancelled the authorization (null if not cancelled)

CancellationReasonstring?

Reason for cancellation (null if not cancelled)

CancelAuthorizationResponse

Response after cancelling an authorization.

AuthorizationIdstring

Authorization ID that was cancelled

Vinstring

Vehicle Identification Number

StatusAuthorizationStatus

Status after cancellation (CANCELLED)

CancelledAtDateTimeOffset

Cancellation timestamp

CancelledBystring

User or system that cancelled the authorization

CancellationReasonstring

Reason for cancellation

AuthorizedDriver (ADR-042)

Per-driver entry on a multi-driver authorization.

PersonIdentityKeystringrequired

Mapped person identity key (HMAC form per ADR-043). Used as the key for add / remove / assign operations.

Namestring?

Display-only label (e.g., "Alice Driver"). Stored on the authorization record but does NOT appear in event payloads per ADR-042 AC-16 (custody.authorization.modified and custody.authorization.assigned carry mapped personIdentityKey values only). Useful for populating UI without re-querying upstream identity systems.

UpdateDriversResponse (ADR-042)

Returned by UpdateAuthorizationDriversAsync().

AuthorizationIdstring

Authorization ID that was modified.

Vinstring

Vehicle Identification Number.

StatusAuthorizationStatus

Always PENDING post-PATCH (PATCH is rejected with 409 AUTHORIZATION_ALREADY_ASSIGNED if the authorization is in any other state).

AuthorizedDriversList<AuthorizedDriver>

Post-PATCH driver list (state after add/remove operations applied).

Versionint

New monotonic version. Use as the next Version argument.

DriverAuthorizationModestring

Stamped mode (multi_driver for any authorization that supports PATCH).

ActorIdentifierstring?

Caller-asserted forensic identifier (preserved across omitted-actor calls). Not verified.

AssignAuthorizationResponse (ADR-042)

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

AuthorizationIdstring

Authorization ID that was assigned.

Vinstring

Vehicle Identification Number.

StatusAuthorizationStatus

ASSIGNED on success.

AssignedDriverPikstring

Raw PersonIdentityKey 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.

DriverAuthorizationModestring

Stamped mode (single_driver or multi_driver).

ActorIdentifierstring?

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

RevokedCredentialIdsList<string>

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 AssignPartialFailureException.FailedRevocations.

Common Use Cases

Auction to Dealership Transfer

public async Task<string> CreateAuctionTransfer(
    string vin,
    string driverIdentity)
{
    var request = new CreateAuthorizationRequest
    {
        Vin = vin,
        Origin = "1180 Lake Hearn Dr NE, Atlanta, GA 30342",
        Destination = "4101 Millenia Blvd, Orlando, FL 32839",
        PersonIdentityKey = driverIdentity,
        MakeModel = "Toyota Camry",
        TransportOrderId = $"TO-{DateTime.UtcNow:yyyyMMdd}-001",
        AuthorizedBy = "auction-integration-service",
        ValidUntil = DateTimeOffset.UtcNow.AddHours(72),  // 3 days
        Role = AuthorizationRole.DRIVER
    };

    var auth = await _client.CreateAuthorizationAsync(request);

    Console.WriteLine($"Authorization {auth.AuthorizationId} created");
    Console.WriteLine($"Expires at: {auth.ExpiresAt:yyyy-MM-dd HH:mm}");

    return auth.AuthorizationId;
}

Wait for Authorization Release

public async Task<AuthorizationDetails> CreateAndWaitForRelease(
    CreateAuthorizationRequest request)
{
    // Create authorization
    var auth = await _client.CreateAuthorizationAsync(request);

    Console.WriteLine($"Authorization {auth.AuthorizationId} created with status {auth.Status}");

    // Wait for authorization to be released
    var options = new WaitOptions
    {
        PollingIntervalSeconds = 5,
        TimeoutSeconds = 300,
        CustomPredicate = a => a.Status == AuthorizationStatus.RELEASED
    };

    var details = await _client.WaitForAuthorizationAsync(
        auth.AuthorizationId,
        options);

    Console.WriteLine($"Authorization {details.AuthorizationId} released");

    return details;
}

Monitor Authorization Status

public async Task MonitorAuthorization(string authorizationId)
{
    while (true)
    {
        var details = await _client.GetAuthorizationAsync(authorizationId);

        Console.WriteLine($"Status: {details.Status}");

        if (details.Status == AuthorizationStatus.RELEASED ||
            details.Status == AuthorizationStatus.CANCELLED ||
            details.Status == AuthorizationStatus.EXPIRED ||
            details.Status == AuthorizationStatus.REVOKED)
        {
            Console.WriteLine("Authorization reached terminal status");
            break;
        }

        await Task.Delay(TimeSpan.FromSeconds(10));
    }
}

Error Handling Pattern

using CoxAuto.Vecu.CustodySdk.Exceptions;

public async Task<AuthorizationResponse?> CreateAuthorizationWithRetry(
    CreateAuthorizationRequest request,
    int maxRetries = 3)
{
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            return await _client.CreateAuthorizationAsync(request);
        }
        catch (DuplicateAuthorizationException ex)
        {
            // Authorization already exists - not retryable
            Console.WriteLine($"Duplicate authorization: {ex.Message}");
            throw;
        }
        catch (VehicleNotReleasableException ex)
        {
            // Vehicle not releasable - not retryable
            Console.WriteLine($"Vehicle not releasable: {ex.Message}");
            throw;
        }
        catch (RateLimitException ex)
        {
            // Rate limited - wait and retry
            if (attempt < maxRetries)
            {
                var delay = ex.RetryAfter ?? TimeSpan.FromSeconds(30);
                Console.WriteLine($"Rate limited, retrying after {delay}");
                await Task.Delay(delay);
            }
        }
        catch (NetworkException ex)
        {
            // Network error - retry with backoff
            if (attempt < maxRetries)
            {
                var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
                Console.WriteLine($"Network error, retrying in {delay}");
                await Task.Delay(delay);
            }
        }
    }

    return null;
}

Multi-Driver Exception Hierarchy (ADR-042)

Multi-driver operations introduce additional typed exceptions. Most inherit from intermediate base classes — e.g., MultiDriverModeRequiredException is a subclass of InvalidConfigurationException, mirroring the existing InvalidOriginAddressException precedent.

ExceptionHTTPServer error codeRaised when
MultiDriverModeRequiredException400MULTI_DRIVER_MODE_REQUIREDCaller's client is in single_driver mode but the request used a multi-driver endpoint or supplied AuthorizedDrivers.
EmptyDriverListException400EMPTY_DRIVER_LISTUpdateAuthorizationDriversAsync net effect would leave zero drivers.
DriverNotOnListException400DRIVER_NOT_ON_LISTAssignAuthorizationToDriverAsync requested a driver not on the authorization's AuthorizedDrivers list.
NotAuthorizationOwnerException403NOT_AUTHORIZATION_OWNERCaller's client_id does not own the authorization (cross-tenant attempt).
AuthorizationAlreadyAssignedException409AUTHORIZATION_ALREADY_ASSIGNEDUpdateAuthorizationDriversAsync against an ASSIGNED authorization.
AuthorizationAlreadyAssignedToDifferentDriverException409AUTHORIZATION_ALREADY_ASSIGNED_TO_DIFFERENT_DRIVERAssignAuthorizationToDriverAsync requested a different driver on an already-ASSIGNED authorization.
AuthorizationNotAssignableException409AUTHORIZATION_NOT_ASSIGNABLEAssignAuthorizationToDriverAsync against a terminal-status authorization.
StaleETagException412STALE_ETAGVersion argument does not match the authorization's current version. Body carries CurrentVersion for retry.
DriverLimitExceededException429DRIVER_LIMIT_EXCEEDEDMulti-driver POST or UpdateAuthorizationDriversAsync add would exceed per-client driver cap.
PoolFanOutException503POOL_FANOUT_FAILUREMulti-driver POST pool fan-out failed and was rolled back. Caller can retry the entire POST.
AssignPartialFailureException503ASSIGN_PARTIAL_FAILUREAssignAuthorizationToDriverAsync succeeded at the authorization level but credential revocation completed only partially.
RetryExhaustedExceptionn/a(SDK-side)AddOrCreateDriverAsync exhausted its ETag-mismatch retry budget. Caller should back off and retry.
// Catch the parent class to handle the whole config-error family at once
try
{
    await _client.UpdateAuthorizationDriversAsync(authorizationId, request);
}
catch (InvalidConfigurationException ex)
{
    // Handles MultiDriverModeRequiredException + any future config errors
    Console.WriteLine($"Configuration issue: {ex.Message}");
}

Next Steps