API reference
SecondAppraisal GAP Provider API · version 1.1.0
Referral submission and status tracking for GAP administrators, lenders and carriers, and the Garage Hub Membership roster.
- Every request body is JSON (
Content-Type: application/json). A field the schema does not name is refused with 400unknown_field. A field a referral must never carry (an SSN, a birth date, an account number) is refused first, with 400prohibited_field, naming the field and never its value. - Money is integer cents. Timestamps are ISO 8601 in UTC.
- Every refusal the API makes is an RFC 9457 problem (
application/problem+json): a stablecode, theerrorsentence,field_errorson validation, and therequest_idthat is also sent asX-Request-Id.x-error-cataloglists every code. - During a failover, the platform can answer a write (POST or PATCH) itself, before the API sees it: 503 with
Retry-After: 60and a plain JSON body,"error": "platform_standby", with no problem members and noX-Request-Id. Nothing was written: send the same request again afterRetry-Afterseconds, with the sameIdempotency-Keywhere the operation takes one. Reads are not refused this way. - Responses are open: ignore fields you don't know.
- A later release may add values to an enum a response carries; each such enum says what to do with a value you don't recognise.
- Each operation states the key permission it needs (
x-required-permission), its rate class (x-rate-class), its idempotency (x-idempotency), which key modes reach it (x-availability) and the most sensitive data class it carries (x-data-class, described inx-data-classes). Astagedoperation answers 404operation_not_openuntil the switchx-availability.opens_withnames opens. - A field marked
x-requires-permissionis null for a key that doesn't hold the permission it names, wherever the field appears in a response. A legacy key (sa_gap_) gets every such field its scopes reach. - Pipeline state is exposed through the provider-facing milestone timeline only.
This page is generated from the same contract as the OpenAPI document: OpenAPI 3.1 or OpenAPI 3.0.3, for your client generator. Each key may make 300 reads, 120 writes and 12 bulk calls a minute. Wait the number of seconds in Retry-After.
Authentication
bearerAuth
Your API key from the portal's API page: Authorization: Bearer <key>. GET /api/gap/v1/me answers any key, live or test, with what it is, what it may do and when it expires. Keys come in two formats. A legacy key starts sa_gap_ (sa_gap_test_ for a test key). A v2 key, which the portal issues once v2 keys open, is sa_live_ or sa_test_, then 30 letters and digits and a 6-character checksum; a v2 key whose checksum doesn't match gets 401 missing_api_key before any lookup. A v2 key holds only the permissions chosen when it was created, from a preset (Intake only, Intake + status, Reporting, Membership roster) or one by one: each operation names the permission it needs in x-required-permission, and a v2 key without it gets 403 permission_denied. If v2 keys can't be checked just now, the call gets 503 credentials_unavailable; retry after the seconds in Retry-After. A legacy key's scopes decide what it reaches: no scopes reaches everything; referrals the referral and analytics operations; plans the membership plan operations; read the plan list operations and analytics. Outside them it gets 403 key_not_scoped. Every key reaches the events feed, which lists each event type only to a key holding that type's permission, and an event's referral, beyond its id, only to a key holding referrals:read. Test keys (sa_gap_test_...) work only with the membership plan operations, GET /api/gap/v1/me and the events feed's plan events until the referral sandbox opens: until then a referral or analytics call with one gets 403 test_key_referrals_unavailable. Once it opens, a test key reaches sandbox referrals only, and a live key never sees them. A v2 test key (sa_test_...) is a test key in the same way. A test key expires at most 90 days after it is created, a v2 live key at most 365 days after, and a legacy live key only when we schedule its retirement, with notice; an expired key gets 401 api_key_expired. Each key may make 300 reads, 120 writes and 12 bulk calls a minute (x-rate-class); over that is 429 with Retry-After. A signed credential sends no bearer key: it signs each request instead (signedRequest). An institution can ask us to accept signed requests only: from then a call with an API key gets 401 signed_requests_required. A key, bearer or signed, can hold an IP allowlist: a call from an address outside it, as our edge saw it, gets 403 ip_not_allowed. A request that carries a Signature-Input header is read as a signed one, whatever else it carries, so a bearer call must carry none: with one, even a valid key's call gets 401 signature_profile_invalid. Such a call counts as failed authentication from your address, so past the limit it can be 429 auth_failures_throttled. Don't let a proxy, gateway or agent add an HTTP message signature of its own to your bearer calls.
signedRequest
A signed credential: the institution's own Ed25519 or ECDSA P-256 public key, registered in the portal as a JWK or a BEGIN PUBLIC KEY PEM (for P-256, a named-curve (prime256v1) SubjectPublicKeyInfo with an uncompressed point), signing every request with RFC 9421 HTTP Message Signatures and RFC 9530 Content-Digest (the gap-v2 profile). It holds the permissions chosen when it was registered, as a v2 key does, and expires as one does. A new credential is activated by one signed POST /api/gap/v1/credentials/{id}/verify; until then any other call gets 401 credential_not_activated. Each request carries Signature-Input and Signature (one signature, labelled sa), X-SA-Environment (TEST for a test credential, PRODUCTION for a live one) and no Authorization header: a request carrying both credentials is ambiguous, and is 401 signature_profile_invalid however valid either is. The signature covers @method and @target-uri, then content-digest and idempotency-key on POST and PATCH, idempotency-key on GET and HEAD whenever it is sent, then x-sa-environment. Its parameters are created (unix seconds), nonce (16 to 128 of A-Z a-z 0-9 _ -, new for every request), keyid (the credential's id), alg (ed25519 or ecdsa-p256-sha256, the credential's own) and tag="gap-v2", in that order. @target-uri is https://, the host you called in lower case (secondappraisal.com, gap.secondappraisal.com or lenders.secondappraisal.com), then the path and query of the request URL as a WHATWG URL parser serializes it (new URL() in JavaScript), which is how we read every request: never the request line as your client wrote it. Percent-encode each query value with RFC 3986's unreserved characters only (a ' is %27), percent-encode non-ASCII as its UTF-8 bytes, and send no empty ? and no . or .. path segment; a percent-escape you send is kept as sent, its hex digits' case included. On a signed request Idempotency-Key is 1 to 255 visible ASCII characters (! to ~), no spaces. Content-Digest is sha-256=:<base64>: over the exact body bytes, a string's as UTF-8. An Ed25519 signature is its 64 bytes; a P-256 signature is the 64-byte r||s, never DER. Both are sent as sa=:<base64>:. created must be within 300 seconds before our clock and 30 after it. A nonce is used once per credential: it is spent when the signature has verified, the body has matched Content-Digest, the request has come from an address the credential's IP allowlist holds (when it holds one) and the credential was within its rate limit, whatever the request is then answered. A 403 ip_not_allowed, a 503 credentials_unavailable because the allowlist couldn't be decided, a 429 rate_limited, or a 503 platform_standby because the nonce itself couldn't be written, leaves it unspent. A retry is signed again, with a new nonce and the same Idempotency-Key. Refused before any credential is looked up: 401 signature_profile_invalid, signature_expired or target_uri_not_allowed. Refused the same way whatever failed: 401 signature_invalid, before any byte of the body is read, or once the signature verifies, for a body that doesn't match Content-Digest. Either counts as failed authentication from your address, a revoked credential's included, so past the limit it can be 429 auth_failures_throttled, and neither spends the nonce. Then, in order: 413 payload_too_large or 400 invalid_json for a body too large or too slow to arrive, which spends nothing either; 403 ip_not_allowed for an address outside the credential's IP allowlist, or 503 credentials_unavailable when the allowlist can't be decided, which spend nothing either; 429 rate_limited; 409 signature_replay for a nonce already used; 401 api_key_expired, credential_not_activated or environment_mismatch.
At launchSigned credentials, v2 keys and their permissions open with the partner security platform. Until then the portal creates legacy live keys only. A legacy key keeps working after launch: a legacy live key until we schedule its retirement, with notice, and a legacy test key until the expiry GET /api/gap/v1/me reports: 90 days after it was created, or, for a key made before keys had an expiry, 90 days after we stamped it; we may schedule its retirement sooner, with notice.
At launchIP allowlists and signed requests only are enforced from launch. A call from an address outside its key's allowlist gets 403 ip_not_allowed once the origin lock and the partner security platform enforce, and a call with an API key from an institution that accepts signed requests only gets 401 signed_requests_required once the partner security platform enforces. Until then, a call either one would refuse is answered and only logged.
Referrals
Refer a borrower, follow the referral, edit or cancel it.
Create a referral
POST /api/gap/v1/referrals
Available. Live keys only.
At launchTest keys reach sandbox referrals here once the sandbox opens.
Submits one borrower referral. Requires an executed MSA; provider-paid and split-pay referrals also need a billing method. Send an Idempotency-Key header to make retries safe: a replay returns the original referral with idempotent_replay: true and 200. Keys are per institution and per key mode: the same key sent with a live key and a test key makes two referrals, and a live key can't send a key that starts with test: (409 idempotency_key_mode_conflict). A key that doesn't hold referrals:read gets an acknowledgement instead of the referral: its id, object and status, never its data. Such a key's replay must repeat its own request exactly: a key that came with a different request, or with one sent before requests were fingerprinted, is 422 idempotency_key_reused. Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them.
- Authentication
- An API key or a signed credential
- Permission
- referrals:write
- Rate class
- write
- Idempotency
- Idempotency-Key header, at most 255 characters; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | Makes a retry safe: a request with a key your institution already used returns the original result with idempotent_replay: true. At most 255 characters, trimmed; a longer key is refused, never cut. On a signed request (signedRequest) it is required and signed: 1 to 255 visible ASCII characters (! to ~), no spaces, or the call gets 401 signature_profile_invalid. |
Request body
ReferralCreateInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field). A field a referral must never carry is refused before anything else is checked (prohibited_field).
Responses
- 200: ReferralCreateResult or ReferralCreateAcknowledgement. An idempotent replay: the original referral, whatever this body says. For a key that doesn't hold
referrals:read: an acknowledgement, never the referral. - 201: ReferralCreateResult or ReferralCreateAcknowledgement. Created. For a key that doesn't hold
referrals:read: an acknowledgement, never the referral. - The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Members a refusal may add
existing_referral_id(string) on duplicate_referral, only to a key holding referrals:read. Onduplicate_referral: your referral for the same loss. Never sent to a key that doesn't holdreferrals:read.state(string) on state_not_served. Onstate_not_served: the state judged, a two-letter code.basis(string) on state_not_served. Onstate_not_served:garagedwhen the state judged isgaraged_state,losswhen it isloss_state.
Error codes (39)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- prohibited_field · 400 · Prohibited field
- validation_failed · 400 · Validation failed
- idempotency_key_too_long · 400 · Idempotency-Key too long
- msa_required · 403 · Agreement not executed
- billing_required · 403 · Billing method required
- email_required_sms_disabled · 422 · Borrower email required
- script_version_stale · 409 · Disclosure script out of date
- duplicate_reference · 409 · Duplicate referral
- idempotency_key_mode_conflict · 409 · Idempotency-Key used in the other mode
- idempotency_key_reused · 422 · Idempotency-Key reused
- duplicate_referral · 409 · Loss already referred
- state_not_served · 422 · State not served
- internal_error · 500 · Internal error
List referrals
GET /api/gap/v1/referrals
Available. Live keys only.
At launchTest keys reach sandbox referrals here once the sandbox opens.
Your referrals, newest first, a page at a time. Once a borrower withdraws permission to report a referral's progress to you, the referral shows status: reporting_withdrawn and reporting_withdrawn_at, the fields you supplied, and no progress. status matches the status shown: reporting_withdrawn lists those referrals, and any other status leaves them out. Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them.
- Authentication
- An API key or a signed credential
- Permission
- referrals:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| status | query | array of string | Comma-separated statuses, e.g. invited,activated. Case-insensitive. ?status= is no filter; a value naming no status (,, or only spaces) is refused. A status matches the status a referral is shown with: reporting_withdrawn lists the referrals whose borrower withdrew permission to report their progress, and any other status leaves them out.One of: submitted, invited, handoff_pending, outreach_queued, contact_attempted, activated, in_progress, settled, closed, declined, unreachable, expired, cancelled, reporting_withdrawn |
| external_refdeprecated | query | string | Exact match on the external_ref you sent, trimmed. ?external_ref= is no filter; only spaces is refused. Deprecated: the reference travels in the URL, where the systems it passes through can record it. It still filters; retrieve a referral by the id its create returned instead. |
| limit | query | integer | Page size, 1 to 100, written in digits. A blank value means the default. |
| starting_after | query | string | Cursor: the previous page's next_cursor (its last referral's id, or with updated_after an opaque u1. cursor). ?starting_after= is the first page; only spaces is refused. |
| updated_after | query | string (date-time) | Only the referrals changed after this time (ISO 8601 with its offset, such as 2026-10-01T00:00:00Z, from 0001-01-01T00:00:00Z through 9999-12-31T23:59:59.999Z), oldest change first; a referral that changes again before you reach it moves to a later page. A referral whose borrower withdraws permission to report its progress is listed once more, at reporting_withdrawn_at, and never again. A change is listed about a minute after it happens. Every page, the last and an empty one too, returns an opaque u1. next_cursor: to poll, keep the latest one and send it as starting_after with the same updated_after, never a time from your own clock, which misses the changes made in the minute before each poll. A cursor works only for the institution and mode it was issued to; if one is refused (400 invalid_cursor), start again from your updated_after without it. ?updated_after= is no filter. |
Responses
- 200: ReferralList. A newest-first page.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (24)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- validation_failed · 400 · Validation failed
- invalid_cursor · 400 · Invalid cursor
- internal_error · 500 · Internal error
Retrieve a referral
GET /api/gap/v1/referrals/{id}
Available. Live keys only.
At launchTest keys reach sandbox referrals here once the sandbox opens.
The full referral, with the provider-facing milestone timeline and the financial panel. Once a borrower withdraws permission to report a referral's progress to you, the referral shows status: reporting_withdrawn and reporting_withdrawn_at, the fields you supplied, and no progress. Its timeline and financial are then null, as are consultation_number and days_in_negotiation. Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them.
- Authentication
- An API key or a signed credential
- Permission
- referrals:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The referral's id. |
Responses
- 200: ReferralDetail. The referral.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (24)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- internal_error · 500 · Internal error
Update or cancel a referral
PATCH /api/gap/v1/referrals/{id}
Available. Live keys only.
At launchTest keys reach sandbox referrals here once the sandbox opens.
loan_payoff_cents and deductible_cents are editable any time. Every other field, and the program terms, until the borrower activates (409 after). { "action": "cancel" }, sent on its own, cancels before activation (409 after). A referral whose borrower withdrew permission to report its progress is 409 reporting_withdrawn, whatever the request asks, and nothing is written. A key that doesn't hold referrals:read gets an acknowledgement instead of the referral: its id, object and status, never its data. Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them.
- Authentication
- An API key or a signed credential
- Permission
- referrals:write
- Rate class
- write
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The referral's id. |
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
ReferralPatchInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field). A field a referral must never carry is refused before anything else is checked (prohibited_field).
Responses
- 200: ReferralDetail or ReferralAcknowledgement. The updated referral. For a key that doesn't hold
referrals:read: an acknowledgement, never the referral. - The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Members a refusal may add
state(string) on state_not_served. Onstate_not_served: the state judged, a two-letter code.basis(string) on state_not_served. Onstate_not_served:garagedwhen the state judged isgaraged_state,losswhen it isloss_state.
Error codes (37)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- prohibited_field · 400 · Prohibited field
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- nothing_to_update · 400 · Nothing to update
- referral_locked · 409 · Referral locked
- cancel_not_allowed · 409 · Cancel not allowed
- economics_locked · 409 · Program terms locked
- billing_required · 403 · Billing method required
- reporting_withdrawn · 409 · Reporting withdrawn
- state_not_served · 422 · State not served
- internal_error · 500 · Internal error
Create referrals in bulk
POST /api/gap/v1/referrals/bulk
Available. Live keys only.
At launchTest keys reach sandbox referrals here once the sandbox opens.
Up to 500 referrals in one call. Each row is validated and created on its own: valid rows are created even when siblings fail, and results reports every row. A row may carry its own idempotency_key and, for a warm handoff, its disclosure. An institution without an executed MSA gets 403 msa_required for the whole call; a provider-paid or split-pay row without a billing method fails with the row code billing_required. A key that doesn't hold referrals:read gets each row's referral as an acknowledgement (its id, object and status), and its rows replay only their own requests, as a single create's do. Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them.
- Authentication
- An API key or a signed credential
- Permission
- referrals:write
- Rate class
- bulk
- Idempotency
- A per-row key, at most 255 characters; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it: each row's idempotency_key is what makes a retry safe. |
Request body
ReferralBulkInput (application/json, required). Content-Type: application/json, at most 1,048,576 bytes. A field the schema does not name is refused (unknown_field). A field a referral must never carry is refused before anything else is checked (prohibited_field).
Responses
- 200: ReferralBulkResult or ReferralBulkAcknowledgement. Row-level results. For a key that doesn't hold
referrals:read: an acknowledgement, never the referral. - The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (31)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- prohibited_field · 400 · Prohibited field
- validation_failed · 400 · Validation failed
- too_many_rows · 413 · Too many rows
- msa_required · 403 · Agreement not executed
- internal_error · 500 · Internal error
Simulate a sandbox referral's next step
POST /api/gap/v1/referrals/{id}/simulate
At launchIt opens with the referral sandbox. Until then every call answers 404 operation_not_open. Test keys only.
Test keys only. Moves one of your sandbox referrals one step along its lifecycle, and sends the webhook event a live referral sends at that step (referral.invited, referral.activated, referral.declined or settlement.updated) to your test endpoints, marked livemode: false. Nothing reaches a borrower and nothing is billed. A key that doesn't hold referrals:read gets an acknowledgement instead of the referral: its id, object and status, never its data. A live key gets 403 simulate_requires_test_key, an id that is not one of your sandbox referrals 404, and a step off the lifecycle 409 invalid_transition. A referral whose borrower withdrew permission to report its progress is 409 reporting_withdrawn, whatever the request asks, and nothing is written. Staged: it opens with the referral sandbox, and until then every call answers 404 operation_not_open.
- Authentication
- An API key or a signed credential
- Permission
- referrals:write
- Rate class
- write
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The referral's id. |
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
ReferralSimulateInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: ReferralDetail or ReferralAcknowledgement. The sandbox referral after the step. For a key that doesn't hold
referrals:read: an acknowledgement, never the referral. - The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (33)
- operation_not_open · 404 · Operation not open yet
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- simulate_requires_test_key · 403 · Test key required
- invalid_transition · 409 · Invalid transition
- reporting_withdrawn · 409 · Reporting withdrawn
- internal_error · 500 · Internal error
Attest a warm handoff
POST /api/gap/v1/referrals/{id}/attest
At launchIt opens with the referral sandbox. Until then every call answers 404 operation_not_open. Live and test keys.
For a warm-handoff referral created without its disclosure: you certify that your staff delivered the disclosure script and that the borrower agreed to be contacted, the same attestation as create's disclosure, and the referral is queued for our outreach. Accepted only while the referral waits for its attestation: after that, or for an invitation referral, it is a 409 invalid_transition. A script version that is not the current script is a 409 script_version_stale. A referral whose borrower withdrew permission to report its progress is 409 reporting_withdrawn, whatever the request asks, and nothing is written. A test key attests its sandbox referrals, which reach no one: no outreach is opened for them. A key that doesn't hold referrals:read gets an acknowledgement instead of the referral: its id, object and status, never its data. Staged: it opens with the referral sandbox, and until then every call answers 404 operation_not_open.
- Authentication
- An API key or a signed credential
- Permission
- referrals:write
- Rate class
- write
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The referral's id. |
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
DisclosureInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field). A field a referral must never carry is refused before anything else is checked (prohibited_field).
Responses
- 200: ReferralDetail or ReferralAcknowledgement. The referral, queued for our outreach. For a key that doesn't hold
referrals:read: an acknowledgement, never the referral. - The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (34)
- operation_not_open · 404 · Operation not open yet
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- prohibited_field · 400 · Prohibited field
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- script_version_stale · 409 · Disclosure script out of date
- invalid_transition · 409 · Invalid transition
- reporting_withdrawn · 409 · Reporting withdrawn
- internal_error · 500 · Internal error
Look up review status by VIN, claim number or reference
POST /api/gap/v1/review-status/lookup
At launchIt opens with review records. Until then every call answers 404 operation_not_open. Live and test keys.
Finds your referrals by one identifier, sent in the body so it never travels in a URL: vin, claim_number or external_ref, as you sent it on the referral. Each match comes with the latest version of its review record, cited as { id, version, hash } with its status and when it was written, and with its close reason: what the review webhooks carry. Read the version itself with GET /api/gap/v1/referrals/{id}/review-record. Newest first, at most 100 (has_more says more matched); no match is an empty list. record is null while a referral's record has no version to show: before its first, and from the borrower's request that we stop reporting the referral's progress until the version that records it. It writes nothing, but during a failover the platform can still answer it as it answers a write (503 platform_standby). Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them. Staged: it opens with review records, once their sweep is on and has finished its first pass; until then every call answers 404 operation_not_open.
- Authentication
- An API key or a signed credential
- Permission
- referrals:read
- Rate class
- read
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_personal
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
ReviewStatusLookupInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: ReviewStatusList. Your matching referrals, newest first, each with its review status.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (27)
- operation_not_open · 404 · Operation not open yet
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Retrieve a referral's review record
GET /api/gap/v1/referrals/{id}/review-record
At launchIt opens with review records. Until then every call answers 404 operation_not_open. Live and test keys.
One version of the referral's review record: the latest, or the one ?version= names. A version is written each time the record changes and never edited, and its hash is the SHA-256 of bundle's canonical JSON (RFC 8785), so the version you read later still matches the hash a webhook or the lookup cited. bundle holds the record: its status, the verdict once it reached the borrower and whether we can help (never a dollar figure), the review window, outreach, a refusal, a review done elsewhere, the appraisal right, the costs by component and payer, the requirement, the outcome and the copy versions. A redacted version keeps its id, version, hash and status, with bundle: null. Once the borrower asks us to stop reporting the referral's progress, the latest version says only that, and the versions before it stay readable by number; until the version that records it is written, there is no latest to show. 404 not_found for a referral that isn't yours in this key's mode, a version it doesn't have, or a record with no version to show yet. costs.engagement and costs.institution_charges are null unless the key also holds reporting:read. The hash covers the whole record, costs included, so a copy with them null still cites the version but doesn't hash to it. Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them. Staged: it opens with review records, once their sweep is on and has finished its first pass; until then every call answers 404 operation_not_open.
- Authentication
- An API key or a signed credential
- Permission
- referrals:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- borrower_personal
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The referral's id. |
| version | query | integer | The version to read, 1 for the record's first, written in digits. Leave it out, or blank, for the latest the record can show. |
Responses
- 200: ReviewRecord. The version.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (25)
- operation_not_open · 404 · Operation not open yet
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- internal_error · 500 · Internal error
Analytics
Your funnel, outcomes and spend.
Savings and spend analytics
GET /api/gap/v1/analytics
Available. Live keys only.
At launchTest keys reach sandbox referrals here once the sandbox opens.
Your referral funnel and activation rate, settled outcomes (uplift, exposure reduction), spend (fees, ROI multiple, net savings), and breakdowns by market and carrier: the numbers the portal's Analytics page shows. Filter on the referral's creation date with from (inclusive) and to (exclusive). Test keys (sa_gap_test_... or sa_test_...) get 403 test_key_referrals_unavailable until the referral sandbox opens; from then on a test key reaches sandbox referrals only, and a live key never sees them.
- Authentication
- An API key or a signed credential
- Permission
- reporting:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- institution_confidential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| from | query | string (date) | Referrals created on or after this date (UTC), YYYY-MM-DD. ?from= is no bound. Spaces around it are ignored. |
| to | query | string (date) | Referrals created before this date (UTC), YYYY-MM-DD: the bound is exclusive. Must be after from. ?to= is no bound. Spaces around it are ignored. |
Responses
- 200: AnalyticsReport. The report.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (23)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- test_key_referrals_unavailable · 403 · Test keys can't reach referrals yet
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Garage Hub Membership
The Garage Hub Membership: the vehicles you enroll, the consultations drawn against them and your monthly statements. The membership is not insurance: nothing is paid to a member and no result is promised.
Enroll one vehicle
POST /api/gap/v1/plans/enrollments
Available. Live and test keys.
Idempotent per institution and key mode with the Idempotency-Key header: a replay returns the original with idempotent_replay: true and 200; a key already used in the other mode or for another VIN is a 422 idempotency_key_reused. Partner-billed institutions get an ACTIVE enrollment at once (you are the payer of record); direct-collect institutions get a PENDING one with an activation_url for the member.
- Authentication
- An API key or a signed credential
- Permission
- plans:write
- Rate class
- write
- Idempotency
- Idempotency-Key header, at most 255 characters; a signed request sends and signs an Idempotency-Key
- Data class
- credential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | Makes a retry safe: a request with a key your institution already used returns the original result with idempotent_replay: true. At most 255 characters, trimmed; a longer key is refused, never cut. On a signed request (signedRequest) it is required and signed: 1 to 255 visible ASCII characters (! to ~), no spaces, or the call gets 401 signature_profile_invalid. |
Request body
PlanEnrollmentInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: PlanEnrollmentCreateResult. An idempotent replay: the original enrollment.
- 201: PlanEnrollmentCreateResult. Enrolled.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (42)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- membership_not_enabled · 403 · Membership not enabled
- membership_rider_unsigned · 403 · Membership rider unsigned
- membership_billing_mode_required · 403 · Membership billing mode required
- membership_billing_method_required · 403 · Billing method required
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- idempotency_key_too_long · 400 · Idempotency-Key too long
- vin_already_live · 409 · VIN already enrolled
- vin_already_consulted · 422 · VIN already consulted
- state_required · 422 · Garaged state required
- state_blocked · 422 · State not served
- vin_invalid · 422 · Invalid VIN
- invalid_customer_price · 422 · Invalid member price
- idempotency_key_reused · 422 · Idempotency-Key reused
- plan_not_enabled · 403 · Membership not enabled
- internal_error · 500 · Internal error
List your roster
GET /api/gap/v1/plans/enrollments
Available. Live and test keys.
Your enrollments in this key's mode, newest first. A key scoped to read may list, and an institution whose membership gate is closed can still read its roster.
- Authentication
- An API key or a signed credential
- Permission
- plans:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- credential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| vindeprecated | query | string | Only this VIN: 17 characters; case, spaces, dashes and dots are ignored. The check digit is not checked. ?vin= is no filter. Deprecated: the VIN travels in the URL, where the systems it passes through can record it. It still filters; retrieve an enrollment by the id its create returned instead. |
| status | query | string | Only this status, case-insensitive. ?status= is no filter.One of: PENDING, ACTIVE, PAST_DUE, LAPSED, CANCELLED |
| cursor | query | string | The previous page's next_cursor. ?cursor= is the first page; only spaces is refused. |
| limit | query | integer | Page size, 1 to 200, written in digits. A blank value means the default. |
Responses
- 200: PlanEnrollmentList. A newest-first page.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (24)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Retrieve one enrollment
GET /api/gap/v1/plans/enrollments/{id}
Available. Live and test keys.
One of your enrollments in this key's mode.
- Authentication
- An API key or a signed credential
- Permission
- plans:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- credential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The enrollment's id. |
Responses
- 200: PlanEnrollment. The enrollment.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (25)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- internal_error · 500 · Internal error
Cancel, swap the VIN, or edit contact details
PATCH /api/gap/v1/plans/enrollments/{id}
Available. Live and test keys.
{ "cancel": true }, on its own, cancels: at once for a partner-billed enrollment, at period end for a direct-collect one. { "new_vin": … } enrolls the new VIN and closes this enrollment (201): the new vehicle's eligibility window starts again; garaged_state, external_ref and each member field carry over unless sent, and a paying direct-collect member's subscription moves to the new enrollment. Any other body edits external_ref and member only; a body with nothing to apply is a 400. In external_ref, send your own enrollment or file ID. Never a loan, account or policy number (a credit union member number is an account number).
- Authentication
- An API key or a signed credential
- Permission
- plans:write
- Rate class
- write
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- credential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The enrollment's id. |
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
PlanEnrollmentPatchInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: PlanEnrollment. The cancelled or edited enrollment.
- 201: PlanEnrollmentSwapResult. Swapped: the new enrollment, naming the one it replaced.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (44)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- membership_not_enabled · 403 · Membership not enabled
- membership_rider_unsigned · 403 · Membership rider unsigned
- membership_billing_mode_required · 403 · Membership billing mode required
- membership_billing_method_required · 403 · Billing method required
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- not_found · 404 · Not found
- not_live · 409 · Enrollment not live
- nothing_to_update · 400 · Nothing to update
- vin_already_live · 409 · VIN already enrolled
- vin_already_consulted · 422 · VIN already consulted
- state_required · 422 · Garaged state required
- state_blocked · 422 · State not served
- vin_invalid · 422 · Invalid VIN
- invalid_customer_price · 422 · Invalid member price
- idempotency_key_reused · 422 · Idempotency-Key reused
- plan_not_enabled · 403 · Membership not enabled
- internal_error · 500 · Internal error
Enroll up to 500 vehicles
POST /api/gap/v1/plans/enrollments/bulk
Available. Live and test keys.
Each row is validated and enrolled on its own; results reports every row. A row's idempotency_key replays it like the single endpoint's header. An Idempotency-Key header (at most 251 characters) keys every row without its own as <header>:<index>, so re-sending the same batch replays it.
- Authentication
- An API key or a signed credential
- Permission
- plans:write
- Rate class
- bulk
- Idempotency
- Idempotency-Key header (at most 251 characters), or a per-row key (at most 255); a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | A batch key: every row without its own idempotency_key is keyed <header>:<index>, so re-sending the same batch replays it. At most 251 characters, trimmed. On a signed request (signedRequest) it is required and signed: 1 to 255 visible ASCII characters (! to ~), no spaces, or the call gets 401 signature_profile_invalid. |
Request body
PlanEnrollmentBulkInput (application/json, required). Content-Type: application/json, at most 1,048,576 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: PlanEnrollmentBulkResult. Row-level results.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (35)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- membership_not_enabled · 403 · Membership not enabled
- membership_rider_unsigned · 403 · Membership rider unsigned
- membership_billing_mode_required · 403 · Membership billing mode required
- membership_billing_method_required · 403 · Billing method required
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- too_many_rows · 413 · Too many rows
- idempotency_key_too_long · 400 · Idempotency-Key too long
- internal_error · 500 · Internal error
Reconcile your full roster
POST /api/gap/v1/plans/roster-sync
Available. Live and test keys.
Every VIN in the list ends up live; every live VIN of yours not in the list is cancelled (you stopped billing that member), at period end for a paying direct-collect member. Only an explicit { "enrollments": [] } empties the roster; a bare array is refused. Any malformed row refuses the whole sync (400 with rejected), so a parse error can never cancel a live member. Built for a nightly job from a loan-servicing system. In external_ref, send your own enrollment or file ID. Never a loan, account or policy number (a credit union member number is an account number).
- Authentication
- An API key or a signed credential
- Permission
- plans:write
- Rate class
- bulk
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_contact
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
PlanRosterSyncInput (application/json, required). Content-Type: application/json, at most 1,048,576 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: PlanRosterSyncResult. What the sync did.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Members a refusal may add
rejected(array of PlanRosterRejection) on validation_failed. Onvalidation_failedfor rows: every row that failed, by index, with its field errors. Nothing was changed.
Error codes (34)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- membership_not_enabled · 403 · Membership not enabled
- membership_rider_unsigned · 403 · Membership rider unsigned
- membership_billing_mode_required · 403 · Membership billing mode required
- membership_billing_method_required · 403 · Billing method required
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- too_many_rows · 413 · Too many rows
- internal_error · 500 · Internal error
Tell us a member vehicle was declared a total loss
POST /api/gap/v1/plans/loss-notices
Available. Live and test keys.
The payoff-request trigger: you usually know before the member does. Records the notice on the enrollment so we can reach the member with a running start. 404 not_a_member_vehicle when the VIN is not a live member vehicle of yours; a date_of_loss more than a day ahead is a 400.
- Authentication
- An API key or a signed credential
- Permission
- plans:write
- Rate class
- write
- Idempotency
- None needed with a bearer key; a signed request sends and signs an Idempotency-Key
- Data class
- borrower_personal
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Key | header | string | On a signed request (signedRequest) it is required and signed, and must be 1 to 255 visible ASCII characters (! to ~), with no spaces, or the call gets 401 signature_profile_invalid. A call with an API key may leave it out or send any value. This operation keeps no record of it, so it makes nothing idempotent here. |
Request body
PlanLossNoticeInput (application/json, required). Content-Type: application/json, at most 65,536 bytes. A field the schema does not name is refused (unknown_field).
Responses
- 200: PlanLossNoticeResult. Recorded.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (35)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- membership_not_enabled · 403 · Membership not enabled
- membership_rider_unsigned · 403 · Membership rider unsigned
- membership_billing_mode_required · 403 · Membership billing mode required
- membership_billing_method_required · 403 · Billing method required
- invalid_json · 400 · Invalid JSON
- unsupported_media_type · 415 · Unsupported media type
- payload_too_large · 413 · Payload too large
- validation_failed · 400 · Validation failed
- vin_invalid · 400 · Invalid VIN
- not_a_member_vehicle · 404 · Not a member vehicle
- internal_error · 500 · Internal error
Member consultations drawn against your roster
GET /api/gap/v1/plans/redemptions
Available. Live and test keys.
Ids, VIN, status, dates and the outcome once settled: never the consultation's internals. Newest first, a page at a time. A redemption names the referral its member consultation made (referral_id); once the borrower withdraws permission to report that referral's progress to you, the redemption shows status: REPORTING_WITHDRAWN and reporting_withdrawn_at, with no outcome and no settled_at, as the referral itself reads (status: reporting_withdrawn).
- Authentication
- An API key or a signed credential
- Permission
- plans:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- borrower_personal
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| cursor | query | string | The previous page's next_cursor. ?cursor= is the first page; only spaces is refused. |
| limit | query | integer | Page size, 1 to 200, written in digits. A blank value means the default. |
Responses
- 200: PlanRedemptionList. A newest-first page.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (24)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Your monthly membership statements
GET /api/gap/v1/plans/statements
Available. Live and test keys.
Your last 36 monthly statements, newest first. A test key gets an empty list: statements bill only live enrollments.
- Authentication
- An API key or a signed credential
- Permission
- plans:read
- Rate class
- read
- Idempotency
- None needed
- Data class
- institution_confidential
Responses
- 200: PlanStatementList. Your statements.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (24)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- key_not_scoped · 403 · Key not scoped for this surface
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- permission_denied · 403 · Permission denied
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- auth_unavailable · 500 · Key check unavailable
- membership_program_closed · 404 · Membership closed to institutions
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Events
What happened to your referrals and enrollments, for polling instead of, or beside, webhooks.
List your events
GET /api/gap/v1/events
At launchIt opens with the partner security platform. Until then every call answers 404 operation_not_open. Live and test keys.
Your institution's events from the last 30 days, oldest first: every event we record for you, whether or not a webhook endpoint takes it, as a thin event (ids, your external_ref, statuses, the milestone reached, outcome codes and timestamps; never a borrower's contact details, a payoff, a VIN or an amount). Each event type needs its own permission: the referral and review events referrals:read, charge.created reporting:read, and the membership plan events plans:read. A key lists only the types it holds the permission for, and a key that holds none of them gets an empty list; a key without referrals:read gets each event's referral as its id only. The membership plan events are listed only while the membership program is open to your institution, and a test key lists the referral and charge events only once the referral sandbox opens, as those operations answer. The review events (review.completed, review.updated and referral.closed) are listed only while review records are open, as the review-status lookup and the review record answer. Once a borrower withdraws permission to report a referral's progress, its events from before the withdrawal stay listed, and from the withdrawal on, only the event that announces it is listed (referral.closed, or review.updated citing the REPORTING_WITHDRAWN version); its charge.created and the membership plan's enrollment and statement events stay, while plan.benefit.redeemed and plan.benefit.completed follow the referral the redemption made. A live key lists live events, and a test key sandbox ones. An event is listed about a minute after it happens. To poll, keep the next_cursor every page returns and send it as starting_after, never an event's id: an event can reach the feed after a newer one's webhook has gone out, so a position taken from a webhook would skip it. Webhooks and the feed carry the same event ids, so drop duplicates by id across both. Which types are listed is decided when you read: the events of a surface closed then are not listed later to a poller that has read past them. To see them, or after a 400 invalid_cursor (a cursor works only for the institution and mode it was issued to), start again without starting_after: the feed lists the last 30 days, and you drop the ids you have. Staged: it opens with the partner security platform, and until then every call answers 404 operation_not_open.
- Authentication
- Any API key or signed credential, live or test
- Permission
- None
- Rate class
- read
- Idempotency
- None needed
- Data class
- institution_confidential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| limit | query | integer | Page size, 1 to 100, written in digits. A blank value means the default. |
| starting_after | query | string | The previous page's next_cursor: an opaque position, the last event this feed listed to you; the page starts after it. Never an event's id, not even one a webhook delivered: an event can reach the feed after a newer one's webhook has gone out, so a position taken from a webhook would skip it. A cursor works only for the institution and mode it was issued to; anything else is 400 invalid_cursor. ?starting_after= is the first page; only spaces is refused. |
Responses
- 200: EventFeedPage. An oldest-first page of thin events.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (22)
- operation_not_open · 404 · Operation not open yet
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- validation_failed · 400 · Validation failed
- invalid_cursor · 400 · Invalid cursor
- internal_error · 500 · Internal error
The OpenAPI document
This document.
This OpenAPI document
GET /api/gap/v1/openapi
Available. Reached without a key.
The OpenAPI 3.1 document for this API, generated from the contract the API runs on. No key needed. ?oas=3.0 serves a derived OpenAPI 3.0.3 copy.
- Authentication
- No key needed
- Permission
- None
- Rate class
- none
- Idempotency
- None needed
- Data class
- public
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| oas | query | string | 3.0 serves the derived OpenAPI 3.0.3 copy, for tooling that predates 3.1.One of: 3.1, 3.0 |
Responses
- 200: OpenApiDocument. The document.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (3)
- edge_auth_required · 403 · Edge authentication required
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
This OpenAPI document (.json alias)
GET /api/gap/v1/openapi.json
Available. Reached without a key.
The same document as GET /api/gap/v1/openapi, under a path ending in .json for tools that expect one.
- Authentication
- No key needed
- Permission
- None
- Rate class
- none
- Idempotency
- None needed
- Data class
- public
- Alias of
- getOpenApiDocument
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| oas | query | string | 3.0 serves the derived OpenAPI 3.0.3 copy, for tooling that predates 3.1.One of: 3.1, 3.0 |
Responses
- 200: OpenApiDocument. The document.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (3)
- edge_auth_required · 403 · Edge authentication required
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Keys and credentials
The key making the call: what it is, what it may do and when it expires; and a signed credential's activation.
The key making this call
GET /api/gap/v1/me
Available. Live and test keys.
Which API key made the call and what it may do: its id and prefix, live or test, its format and how it authenticates, the permissions it holds (a legacy key's scopes as the permissions they reach), when it expires, your institution's id and status, and how many address ranges its IP allowlist holds. Any key reaches it, live or test, whatever it may do, so it is the call to make first when a key is refused elsewhere. A terminated institution's key gets 403 provider_terminated.
- Authentication
- Any API key or signed credential, live or test
- Permission
- None
- Rate class
- read
- Idempotency
- None needed
- Data class
- institution_confidential
Responses
- 200: ApiKeyIntrospection. The key.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (20)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- credential_not_activated · 401 · Credential not activated
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Activate a signed credential
POST /api/gap/v1/credentials/{id}/verify
At launchIt opens when signed credentials open. Until then every call answers 404 operation_not_open. Live and test signed credentials.
A signed credential registered in the portal starts unactivated: until this call succeeds, every other call it signs gets 401 credential_not_activated. Sign this call with the credential itself, {id} being its id (the keyid it signs with), with an empty body: the signature proves you hold the private key of the public key you registered. It answers the credential with activated_at, and a later call answers the same time. A public key is active for one credential at most: if another credential has already activated it, this call gets 409 public_key_in_use, and you register a new key pair instead. Any other caller, a bearer key or another credential, gets 404. Staged: it opens when signed credentials open, and until then every call answers 404 operation_not_open.
- Authentication
- A signed credential, signing this call about itself
- Permission
- None
- Rate class
- write
- Idempotency
- Idempotency-Key required and signed; this call keeps no record of it
- Data class
- institution_confidential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| idrequired | path | string | The signed credential's id: the keyid its signatures name. |
| Idempotency-Keyrequired | header | string | On a signed request (signedRequest), the only kind this call takes, it is required and signed: 1 to 255 visible ASCII characters (! to ~), no spaces, or the call gets 401 signature_profile_invalid. This call keeps no record of it: it is idempotent itself, and a retry is signed again, with a new nonce. |
Responses
- 200: CredentialActivation. The credential, activated.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (26)
- operation_not_open · 404 · Operation not open yet
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- payload_too_large · 413 · Payload too large
- invalid_json · 400 · Invalid JSON
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- provider_suspended · 403 · Account suspended
- provider_not_active · 403 · Account not active
- not_found · 404 · Not found
- public_key_in_use · 409 · Public key in use
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Check a signature (test credentials)
POST /api/gap/v1/signature-check
Planned: not served yet. It is documented ahead of its route. Test signed credentials only.
Planned. For a test credential while you build your signer: send a signed request here and it answers whether the signature verified, with the signature base built from the request as received, to compare byte for byte with yours, and the code the same request would be refused with elsewhere. Test credentials only. Not served yet.
- Authentication
- A signed credential, signing this call about itself
- Permission
- None
- Rate class
- read
- Idempotency
- Idempotency-Key required and signed; this call keeps no record of it
- Data class
- institution_confidential
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| Idempotency-Keyrequired | header | string | On a signed request (signedRequest), the only kind this call takes, it is required and signed: 1 to 255 visible ASCII characters (! to ~), no spaces, or the call gets 401 signature_profile_invalid. This call keeps no record of it: it is idempotent itself, and a retry is signed again, with a new nonce. |
Responses
- 200: SignatureCheckResult. What the signature looked like to the API.
- The API's refusals: an RFC 9457 problem (Problem) with one of the codes below.
- The platform's own answers come before the API's, in plain JSON with an error member and no code: platform_standby (503) to a write during a failover, and edge_auth_required (403) once the origin lock enforces, at launch; and our edge's service_unavailable (503) during an outage.
Error codes (21)
- missing_api_key · 401 · Missing API key
- invalid_api_key · 401 · Invalid API key
- api_key_expired · 401 · API key expired
- provider_terminated · 403 · Account terminated
- rate_limited · 429 · Rate limit exceeded
- credentials_unavailable · 503 · Credentials unavailable
- platform_standby · 503 · Platform on standby
- edge_auth_required · 403 · Edge authentication required
- auth_failures_throttled · 429 · Too many failed authentications
- signature_profile_invalid · 401 · Signature doesn't follow the profile
- signature_expired · 401 · Signature outside its window
- target_uri_not_allowed · 401 · Host not accepted for signed requests
- signature_invalid · 401 · Signature invalid
- environment_mismatch · 401 · Wrong environment
- signature_replay · 409 · Signature replayed
- payload_too_large · 413 · Payload too large
- invalid_json · 400 · Invalid JSON
- ip_not_allowed · 403 · Address not allowed
- signed_requests_required · 401 · Signed requests required
- validation_failed · 400 · Validation failed
- internal_error · 500 · Internal error
Webhook events
Every delivery is a signed POST. The data shapes listed here are the full payloads an endpoint created before launch receives, in the WebhookEvent envelope. Verifying webhooks shows how to check each signature.
At launchA new endpoint receives thin payloads instead: each event as ThinEvent, the same thin event the events feed lists, with ids, statuses and timestamps. Fetch the record for the rest.
referral.invited· The borrower was sent the activation invitation. Data: WebhookReferralInvitedDatareferral.activated· The borrower activated: the consultation exists. Data: WebhookReferralActivatedDatareferral.declined· The borrower declined. Data: WebhookReferralDataconsultation.status_changed· The referral's provider-facing milestone moved. Data:objectThe referral and the milestone it reached. Provisional: the key set is not frozen yet, so read it defensively.appraisal.completed· Our appraisal report is complete. Data:objectThe referral and the milestone it reached. Provisional: the key set is not frozen yet, so read it defensively.settlement.updated· The final settlement was recorded. Data: WebhookSettlementDatacharge.created· A charge for the referral was created. Data:objectThe referral and the charge (id,amount_cents,status). A charge on a referral whose borrower withdrew permission to report its progress still comes, with the referral as every read shows it:status: reporting_withdrawnand no progress. Provisional: the key set is not frozen yet, so read it defensively.plan.enrollment.activated· A membership enrollment became active. Data:objectThe enrollment's id, VIN, external_ref, status and dates; never the member's contact details. Provisional: the key set is not frozen yet, so read it defensively.plan.enrollment.past_due· A direct-collect membership's renewal is failing. Data:objectThe enrollment's id, VIN, external_ref, status and dates; never the member's contact details. Provisional: the key set is not frozen yet, so read it defensively.plan.enrollment.cancelled· A membership enrollment was cancelled or lapsed. Data:objectThe enrollment's id, VIN, external_ref, status and dates; never the member's contact details. Provisional: the key set is not frozen yet, so read it defensively.plan.benefit.redeemed· A member's consultation was drawn against their membership. Data:objectThe enrollment, the redemption id and the consultation number. Not sent once the borrower withdrew permission to report the progress of the referral the redemption names. Provisional: the key set is not frozen yet, so read it defensively.plan.benefit.completed· A member consultation finished. Data:objectThe enrollment, the redemption id and the outcome. Not sent once the borrower withdrew permission to report the progress of the referral the redemption names. Provisional: the key set is not frozen yet, so read it defensively.plan.statement.issued· A monthly membership statement was issued. Data:objectThe statement, as the statements operation returns it. Provisional: the key set is not frozen yet, so read it defensively.review.completed· The referral's review completed: its review record has a new version. Data: WebhookReviewRecordDatareview.updated· A review record already complete or closed has a new version. Data: WebhookReviewRecordDatareferral.closed· The referral closed: its review record's new version gives the close reason. Data: WebhookReviewRecordData
Schemas
AnalyticsReport
Your referral funnel, settled outcomes, spend and breakdowns by market and carrier: the numbers the portal's Analytics page shows.
Fields (47)
| Field | Type | Description |
|---|---|---|
| objectrequired | "gap.analytics" | |
| filtersrequired | object | |
| filters.fromrequired | string (date-time) or null | The from you sent, as an instant. |
| filters.torequired | string (date-time) or null | The to you sent, as an instant. |
| funnelrequired | object | |
| funnel.submittedrequired | integer | |
| funnel.awaiting_activationrequired | integer | |
| funnel.activerequired | integer | |
| funnel.settledrequired | integer | |
| funnel.declinedrequired | integer | |
| funnel.unreachablerequired | integer | |
| funnel.expiredrequired | integer | |
| funnel.cancelledrequired | integer | |
| funnel.reporting_withdrawnrequired | integer | Referrals whose borrower withdrew permission to report their progress to you: counted here and in submitted, never under a later status, in activation_rate or among the outcomes. |
| funnel.activation_raterequired | number or null | Activated ÷ (activated + closed without activation), 0 to 1; null before any referral closes. |
| outcomesrequired | object | |
| outcomes.settled_casesrequired | integer | |
| outcomes.total_uplift_centsrequired | integer | |
| outcomes.avg_uplift_centsrequired | integer or null | |
| outcomes.total_exposure_before_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| outcomes.total_exposure_after_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| outcomes.total_exposure_reduction_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| outcomes.avg_exposure_reduction_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| spendrequired | object | |
| spend.fees_paid_centsrequired | integer | |
| spend.fees_pending_centsrequired | integer | |
| spend.roi_multiplerequired | number or null | Exposure reduction ÷ fees paid; null without both.Null for a key without referrals.contact:read. |
| spend.net_savings_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| by_marketrequired | array of object | |
| by_market[].keyrequired | string | The state code or carrier name; unknown when the referral has none. |
| by_market[].referralsrequired | integer | |
| by_market[].settledrequired | integer | |
| by_market[].uplift_centsrequired | integer | |
| by_market[].exposure_reduction_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| by_carrierrequired | array of object | |
| by_carrier[].keyrequired | string | The state code or carrier name; unknown when the referral has none. |
| by_carrier[].referralsrequired | integer | |
| by_carrier[].settledrequired | integer | |
| by_carrier[].uplift_centsrequired | integer | |
| by_carrier[].exposure_reduction_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| time_seriesrequired | array of object | |
| time_series[].monthrequired | string | The UTC month, YYYY-MM. |
| time_series[].submittedrequired | integer | |
| time_series[].activatedrequired | integer | |
| time_series[].settledrequired | integer | |
| time_series[].uplift_centsrequired | integer | |
| time_series[].exposure_reduction_centsrequired | integer or null | Null for a key without referrals.contact:read. |
ApiKeyIntrospection
The API key that made the call: what it is, what it may do, and when it expires. Never the key itself.
Fields (12)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The key's id, the one the portal's API page lists it under. |
| objectrequired | "gap.api_key" | |
| prefixrequired | string | The key's first characters, as the portal shows them: enough to tell your keys apart, never enough to use one. A signed credential's starts pk:, then the start of its public key's thumbprint. |
| moderequired | string | live, or test for a sandbox key. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: live, test |
| formatrequired | string | legacy for an sa_gap_ key, v2 for an sa_live_ or sa_test_ key or a signed credential. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: legacy, v2 |
| auth_methodrequired | string | secret: the key is sent as a bearer token. public_key: a signed credential, whose requests are signed with its registered public key's private half. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: secret, public_key |
| permissionsrequired | array of string | What the key may do. A v2 key holds the permissions chosen when it was created. A legacy key's scopes are given as the permissions they reach: no scopes reach every one. referrals.contact:read and members:activation-link are field permissions: they open no operation, and a key without one reads null in each field whose x-requires-permission names it, in whatever its operations answer. A legacy read key lists referrals.contact:read for the analytics' figures built on the loan payoff and the roster's member emails, and a legacy plans key for the roster's member emails. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: referrals:write, referrals:read, referrals.contact:read, reporting:read, plans:read, plans:write, members:activation-link |
| expires_atrequired | string (date-time) or null | When the key stops working; null for a legacy live key with no retirement scheduled. |
| institutionrequired | object | |
| institution.idrequired | string | Your institution's id. |
| institution.statusrequired | string | active; requested before approval; suspended, which can read but not write; terminated. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: requested, active, suspended, terminated |
| allowed_cidr_countrequired | integer | How many address ranges the key's IP allowlist holds; 0 when it has none. |
BulkRowAcknowledgementError
Fields (5)
| Field | Type | Description |
|---|---|---|
| fieldrequired | string | The row's field, program, row for the row as a whole, or body for a row that is not an object. |
| messagerequired | string | |
| code | string | A field-error code (unknown_field, invalid_type, required, prohibited_field, …), or a refusal code such as email_required_sms_disabled, script_version_stale, billing_required, idempotency_key_mode_conflict, idempotency_key_reused, state_not_served or duplicate_referral. |
| state | string | On state_not_served: the state judged. |
| basis | string | On state_not_served: garaged when the state judged is garaged_state, loss when it is loss_state. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: garaged, loss |
BulkRowError
Fields (6)
| Field | Type | Description |
|---|---|---|
| fieldrequired | string | The row's field, program, row for the row as a whole, or body for a row that is not an object. |
| messagerequired | string | |
| code | string | A field-error code (unknown_field, invalid_type, required, prohibited_field, …), or a refusal code such as email_required_sms_disabled, script_version_stale, billing_required, idempotency_key_mode_conflict, idempotency_key_reused, state_not_served or duplicate_referral. |
| existing_referral_id | string | On duplicate_referral: your referral for the same loss. Never sent to a key that doesn't hold referrals:read. |
| state | string | On state_not_served: the state judged. |
| basis | string | On state_not_served: garaged when the state judged is garaged_state, loss when it is loss_state. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: garaged, loss |
CredentialActivation
A signed credential, activated: it can sign any call its permissions reach.
Fields (5)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The signed credential's id. |
| objectrequired | "gap.signed_credential" | |
| moderequired | string | live, or test for a sandbox credential. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: live, test |
| algrequired | string | The algorithm its key was registered with, which every signature it makes must name: ed25519 or ecdsa-p256-sha256. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: ed25519, ecdsa-p256-sha256 |
| activated_atrequired | string (date-time) | When the credential's first verification call succeeded. A later call answers the same time. |
DisclosureInput
Warm-handoff attestation. Accepted only with consent_mode: "warm_handoff".
Fields (5)
| Field | Type | Description |
|---|---|---|
| confirmedrequired | true | Must be true. |
| channelrequired | string | How the script was delivered.One of: phone, in_person, email, video, other |
| attestor_namerequired | string | The name of the person on your staff who delivered the script. |
| delivered_on | string (date) or null | The date the script was delivered; omit for today. At most a day ahead (a timezone ahead of UTC) and no more than 90 days old. Spaces around it are ignored. |
| script_version | string or null | The disclosure script version your staff delivered, as the script names it. The referral is stamped with it; omit it to attest the current script. A version that is not the current script is refused with 409 script_version_stale (a row-level error on bulk). |
EventFeedPage
Fields (4)
| Field | Type | Description |
|---|---|---|
| objectrequired | "list" | |
| datarequired | ThinEventarray of ThinEvent | |
| has_morerequired | boolean | |
| next_cursorrequired | string | Every page has one, the last and an empty one too: where your next poll resumes. Keep the latest and send it as starting_after; has_more says whether to fetch the next page now. |
FieldError
One field that failed validation.
Fields (4)
| Field | Type | Description |
|---|---|---|
| fieldrequired | string | The field, in the API's own names: a dotted path inside the body (program.subsidy_value, member.email), or the query or path parameter's name. A rule across fields names the rule (borrower_contact). An error about the body as a whole is body: a body that is not an object, or an edit refused without naming one field. |
| coderequired | string | What is wrong with the field. Branch on this, not on message. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: unknown_field, unknown_parameter, duplicate_parameter, invalid_type, required, too_long, too_large, too_small, invalid_format, invalid_value, prohibited_field |
| messagerequired | string | A sentence a person can act on. |
| pointer | string | RFC 6901 JSON Pointer to the value in the request body (/program/mode); "", the whole body, when field is body. Absent for query and path parameters, and for a rule across fields such as borrower_contact, which names no member of the body. |
OpenApiDocument
This document.
Fields (4)
| Field | Type | Description |
|---|---|---|
| openapirequired | string | |
| inforequired | object | |
| info.titlerequired | string | |
| info.versionrequired | string |
PlanEnrollment
A membership enrollment (snake_case). Ignore fields you don't know.
Fields (25)
| Field | Type | Description |
|---|---|---|
| idrequired | string | |
| vinrequired | string | |
| statusrequired | string | PENDING (direct-collect, awaiting the member's activation), ACTIVE, PAST_DUE, LAPSED or CANCELLED. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PENDING, ACTIVE, PAST_DUE, LAPSED, CANCELLED |
| billing_moderequired | string | PARTNER_BILLED: you pay the wholesale price per vehicle-month and bill the member yourself. DIRECT_COLLECT: we charge the member's card your price and settle the difference with you monthly. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PARTNER_BILLED, DIRECT_COLLECT |
| external_refrequired | string or null | |
| vehiclerequired | object | |
| vehicle.yearrequired | integer or null | |
| vehicle.makerequired | string or null | |
| vehicle.modelrequired | string or null | |
| vehicle.garaged_staterequired | string or null | |
| memberrequired | object | |
| member.first_namerequired | string or null | |
| member.last_namerequired | string or null | |
| member.emailrequired | string or null | Null for a key without referrals.contact:read. |
| member_price_centsrequired | integer | 0 for partner-billed rows. |
| wholesale_centsrequired | integer or null | |
| started_atrequired | string (date-time) or null | |
| eligible_fromrequired | string (date-time) or null | The instant from which a total loss on this vehicle qualifies for the member consultation (thirty days after the membership starts). |
| current_period_endrequired | string (date-time) or null | |
| cancel_at_period_endrequired | boolean | |
| cancelled_atrequired | string (date-time) or null | |
| activation_urlrequired | string or null | Direct-collect only, while PENDING: the co-branded page where the member activates. Anyone holding it can activate, so send it only to the member.Null for a key without members:activation-link. |
| activation_expires_atrequired | string (date-time) or null | |
| testrequired | boolean | Created through a test key. |
| created_atrequired | string (date-time) |
PlanEnrollmentBulkInput
Fields (1)
| Field | Type | Description |
|---|---|---|
| enrollmentsrequired | PlanEnrollmentBulkRowarray of PlanEnrollmentBulkRow | 1 to 500 vehicles. Each row is validated and enrolled on its own. |
PlanEnrollmentBulkResult
Fields (16)
| Field | Type | Description |
|---|---|---|
| receivedrequired | integer | |
| createdrequired | integer | Rows enrolled now (replays are not counted). |
| resultsrequired | array of object | |
| results[].indexrequired | integer | 0-based index into enrollments. |
| results[].vinrequired | string | The row's VIN as sent (normalized when valid). |
| results[].okrequired | boolean | |
| results[].id | string | |
| results[].status | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PENDING, ACTIVE, PAST_DUE, LAPSED, CANCELLED |
| results[].idempotent_replay | boolean | |
| results[].code | string | On a failed row: validation_failed, or the refusal code the single endpoint would answer. |
| results[].message | string | |
| results[].field_errors | array of object | |
| results[].field_errors[].fieldrequired | string | |
| results[].field_errors[].messagerequired | string | |
| results[].field_errors[].code | string | |
| results[].field_errors[].pointer | string |
PlanEnrollmentBulkRow
One vehicle in a bulk enrollment.
Fields (7)
| Field | Type | Description |
|---|---|---|
| vinrequired | string | The 17-character VIN, case-insensitive. Checked before anything is enrolled: 17 characters, no I, O or Q, and a valid check digit (422 vin_invalid, or a field error). |
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| garaged_state | string or null | The two-letter state where the vehicle is garaged, case-insensitive. Required to enroll: a row without it is refused with 422 state_required, and a state where SecondAppraisal can't act as the appraiser with 422 state_blocked. |
| member | PlanMemberInputPlanMemberInput or null | |
| vehicle | PlanVehicleInputPlanVehicleInput or null | |
| consent_mode | string or null | Case-insensitive.One of: invitation, warm_handoff |
| idempotency_key | string or null | Replays this row like the single endpoint's Idempotency-Key header. A row without one is keyed <header>:<index> when the call sends an Idempotency-Key header. |
PlanEnrollmentCancelInput
Cancel the enrollment. Nothing else may be sent with it.
Fields (1)
| Field | Type | Description |
|---|---|---|
| cancelrequired | true | Cancels the enrollment: at once for a partner-billed row, at period end for a direct-collect row with a subscription. |
PlanEnrollmentCreateResult
Every field of PlanEnrollment, and:
Fields (1)
| Field | Type | Description |
|---|---|---|
| idempotent_replayrequired | boolean | True when this answers a replayed Idempotency-Key with the original enrollment. |
PlanEnrollmentEditInput
Edit the reference or the member's contact details. A body with nothing to apply is a 400; member: null, cancel: false or null and new_vin: null apply nothing.
Fields (4)
| Field | Type | Description |
|---|---|---|
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| member | PlanMemberInputPlanMemberInput or null | Null is the same as leaving member out. |
| cancel | false or null | false or null is the same as leaving cancel out. To cancel, send { "cancel": true } on its own. |
| new_vin | null | Null is the same as leaving new_vin out. To swap the vehicle, send the new VIN. |
PlanEnrollmentInput
One vehicle to enroll. The VIN is the key, and the garaged state is required; everything else is optional. A vehicle that has been the subject of a total-loss consultation with SecondAppraisal cannot be enrolled (422 vin_already_consulted); a VIN with a live membership anywhere cannot be enrolled twice (409 vin_already_live).
Fields (6)
| Field | Type | Description |
|---|---|---|
| vinrequired | string | The 17-character VIN, case-insensitive. Checked before anything is enrolled: 17 characters, no I, O or Q, and a valid check digit (422 vin_invalid, or a field error). |
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| garaged_state | string or null | The two-letter state where the vehicle is garaged, case-insensitive. Required to enroll: a row without it is refused with 422 state_required, and a state where SecondAppraisal can't act as the appraiser with 422 state_blocked. |
| member | PlanMemberInputPlanMemberInput or null | |
| vehicle | PlanVehicleInputPlanVehicleInput or null | |
| consent_mode | string or null | Case-insensitive.One of: invitation, warm_handoff |
PlanEnrollmentList
Fields (2)
| Field | Type | Description |
|---|---|---|
| datarequired | PlanEnrollmentarray of PlanEnrollment | |
| next_cursorrequired | string or null | Pass as cursor for the next page; null on the last page. |
PlanEnrollmentPatchInput
One of: { "cancel": true } alone; a VIN swap (new_vin, with optional carry-over fields); or edits to external_ref and member.
One of PlanEnrollmentCancelInput or PlanEnrollmentSwapInput or PlanEnrollmentEditInput.
PlanEnrollmentSwapInput
Move the member to another vehicle: the new VIN is enrolled and this enrollment closes. garaged_state, external_ref and each member field carry over unless sent; a paying direct-collect member's subscription moves to the new row.
Fields (7)
| Field | Type | Description |
|---|---|---|
| new_vinrequired | string | The 17-character VIN, case-insensitive. Checked before anything is enrolled: 17 characters, no I, O or Q, and a valid check digit (422 vin_invalid, or a field error). The member moves to this vehicle, whose eligibility window starts again. |
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| garaged_state | string or null | The two-letter state where the vehicle is garaged, case-insensitive. Required to enroll: a row without it is refused with 422 state_required, and a state where SecondAppraisal can't act as the appraiser with 422 state_blocked. |
| member | PlanMemberInputPlanMemberInput or null | |
| vehicle | PlanVehicleInputPlanVehicleInput or null | |
| consent_mode | string or null | Case-insensitive.One of: invitation, warm_handoff |
| cancel | false or null | false or null is the same as leaving cancel out. To cancel, send { "cancel": true } on its own. |
PlanEnrollmentSwapResult
Every field of PlanEnrollment, and:
Fields (1)
| Field | Type | Description |
|---|---|---|
| swapped_fromrequired | string | The enrollment this one replaced. |
PlanLossNoticeInput
A member vehicle was declared a total loss.
Fields (4)
| Field | Type | Description |
|---|---|---|
| vinrequired | string | The member vehicle's VIN, case-insensitive. |
| date_of_loss | string (date) or null | YYYY-MM-DD; at most a day ahead (a timezone ahead of UTC). Null, or leaving it out, records no date. Spaces around it are ignored. |
| claim_number | string or null | |
| carrier | string or null | The member's insurer. |
PlanLossNoticeResult
Fields (4)
| Field | Type | Description |
|---|---|---|
| enrollment_idrequired | string | |
| statusrequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PENDING, ACTIVE, PAST_DUE, LAPSED, CANCELLED |
| eligible_fromrequired | string (date-time) or null | |
| recordedrequired | true |
PlanMemberInput
The member's contact details. Every field is optional.
Fields (4)
| Field | Type | Description |
|---|---|---|
| first_name | string or null | |
| last_name | string or null | |
string or null | An email address of at most 254 characters, with no spaces, a single @, and a domain with a dot that has text on each side (example.com). Spaces around it are ignored. Stored in lowercase. | |
| phone | string or null |
PlanRedemption
A member consultation drawn against your roster. Never the consultation's internals, and none of its progress once the borrower withdrew permission to report it.
Fields (14)
| Field | Type | Description |
|---|---|---|
| idrequired | string | |
| enrollment_idrequired | string | |
| external_refrequired | string or null | |
| referral_idrequired | string or null | |
| vinrequired | string | |
| statusrequired | string | Where the redemption stands: PENDING, APPROVED, DENIED or COMPLETED. REPORTING_WITHDRAWN once the borrower withdrew permission to report the progress of the referral it names (referral_id), whatever happens after: the referral itself then reads status: reporting_withdrawn. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: PENDING, APPROVED, DENIED, COMPLETED, REPORTING_WITHDRAWN |
| date_of_lossrequired | string (date-time) or null | |
| value_centsrequired | integer | |
| initial_offer_centsrequired | integer or null | |
| final_settlement_centsrequired | integer or null | |
| uplift_centsrequired | integer or null | |
| settled_atrequired | string (date-time) or null | |
| created_atrequired | string (date-time) | |
| reporting_withdrawn_atrequired | string (date-time) or null | When the borrower withdrew permission to report the progress of the referral this redemption names; null while they haven't, or it names none. From then on the redemption shows status: REPORTING_WITHDRAWN, and its outcome (initial_offer_cents, final_settlement_cents, uplift_cents) and settled_at are null. |
PlanRedemptionList
Fields (2)
| Field | Type | Description |
|---|---|---|
| datarequired | PlanRedemptionarray of PlanRedemption | |
| next_cursorrequired | string or null |
PlanRosterRejection
Fields (7)
| Field | Type | Description |
|---|---|---|
| indexrequired | integer | |
| vinrequired | string | |
| field_errorsrequired | array of object | |
| field_errors[].fieldrequired | string | |
| field_errors[].messagerequired | string | |
| field_errors[].code | string | |
| field_errors[].pointer | string |
PlanRosterSyncInput
Fields (1)
| Field | Type | Description |
|---|---|---|
| enrollmentsrequired | PlanEnrollmentInputarray of PlanEnrollmentInput | Your full roster, at most 500 vehicles. An empty array cancels every live enrollment. |
PlanRosterSyncResult
Fields (8)
| Field | Type | Description |
|---|---|---|
| receivedrequired | integer | |
| enrolledrequired | array of string | VINs enrolled now. |
| already_liverequired | array of string | VINs that were already live. |
| refusedrequired | array of object | |
| refused[].vinrequired | string | |
| refused[].coderequired | string | The refusal code the single endpoint would answer. |
| refused[].messagerequired | string | |
| cancelledrequired | array of string | VINs cancelled because they were not in the roster. |
PlanStatement
One calendar month's membership statement, in integer cents. direction says who owes whom: partner_owes (wholesale plus any member-fee subsidy, billed as one charge), we_owe_partner (the direct-collect spread above wholesale, remitted by transfer) or settled.
Fields (18)
| Field | Type | Description |
|---|---|---|
| idrequired | string | |
| period_startrequired | string (date) | |
| period_endrequired | string (date) | |
| statusrequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: ISSUED, SETTLED, VOID |
| vehicle_monthsrequired | number | |
| active_vehicles_endrequired | integer | |
| redemption_countrequired | integer | |
| wholesale_centsrequired | integer | |
| collected_centsrequired | integer | |
| rev_share_centsrequired | integer | |
| subsidy_centsrequired | integer | |
| net_centsrequired | integer | Positive: you owe us; negative: we owe you. |
| directionrequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: partner_owes, we_owe_partner, settled |
| charge_idrequired | string or null | |
| transfer_idrequired | string or null | |
| linesrequired | array of object or null | The statement's lines. Their fields are not frozen yet; ignore any you don't know. |
| issued_atrequired | string (date-time) | |
| settled_atrequired | string (date-time) or null |
PlanStatementList
Fields (1)
| Field | Type | Description |
|---|---|---|
| datarequired | PlanStatementarray of PlanStatement |
PlanVehicleInput
The vehicle's description. Every field is optional.
Fields (4)
| Field | Type | Description |
|---|---|---|
| year | integer or null | |
| make | string or null | |
| model | string or null | |
| trim | string or null |
Problem
RFC 9457 problem details, sent as application/problem+json, with the API's legacy error string and a stable code beside the standard members. Some operations add members of their own, named in their responses.
Fields (9)
| Field | Type | Description |
|---|---|---|
| errorrequired | string | The reason, in the wording the API has always used. Kept for existing clients; branch on code. |
| coderequired | string | Stable reason code. It never changes with the wording; x-error-catalog lists every code. New values may be added: treat a code you don't recognise by the response's HTTP status.One of: invalid_json, unsupported_media_type, payload_too_large, too_many_rows, prohibited_field, validation_failed, operation_not_open, idempotency_key_too_long, internal_error, missing_api_key, invalid_api_key, api_key_expired, key_not_scoped, test_key_referrals_unavailable, provider_terminated, provider_suspended, provider_not_active, rate_limited, auth_unavailable, permission_denied, credentials_unavailable, signed_requests_required, ip_not_allowed, auth_failures_throttled, signature_profile_invalid, signature_expired, target_uri_not_allowed, signature_invalid, environment_mismatch, credential_not_activated, signature_replay, public_key_in_use, edge_auth_required, platform_standby, membership_program_closed, membership_not_enabled, membership_rider_unsigned, membership_billing_mode_required, membership_billing_method_required, msa_required, billing_required, email_required_sms_disabled, script_version_stale, duplicate_reference, invalid_cursor, not_found, referral_locked, cancel_not_allowed, economics_locked, nothing_to_update, idempotency_key_mode_conflict, state_not_served, duplicate_referral, simulate_requires_test_key, invalid_transition, reporting_withdrawn, vin_already_live, vin_already_consulted, state_required, state_blocked, vin_invalid, invalid_customer_price, idempotency_key_reused, plan_not_enabled, not_live, not_a_member_vehicle, request_refused |
| typerequired | string | RFC 9457 problem type: a URI naming the code. It identifies the problem; there is no need to fetch it. |
| titlerequired | string | RFC 9457: a short summary of the code. |
| statusrequired | integer | RFC 9457: the HTTP status, repeated. |
| detailrequired | string | RFC 9457: what went wrong with this request. |
| instancerequired | string | RFC 9457: the path that was called, without its query string. |
| request_idrequired | string | The request id, also sent as the X-Request-Id header. Quote it when you contact us. |
| field_errors | FieldErrorarray of FieldError | On validation_failed and prohibited_field: each field that failed, and why. |
Referral
A referral as the API returns it: snake_case, grouped sub-objects. Ignore fields you don't know.
Fields (45)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The referral's id. |
| objectrequired | "gap.referral" | |
| statusrequired | string | Where the referral stands. reporting_withdrawn once the borrower withdrew permission to report its progress to you, whatever happens to it after: from then on it shows the fields you supplied and no progress. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: submitted, invited, handoff_pending, outreach_queued, contact_attempted, activated, in_progress, settled, closed, declined, unreachable, expired, cancelled, reporting_withdrawn |
| status_labelrequired | string | The status as the portal shows it. |
| status_tonerequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: green, yellow, red, neutral |
| status_noterequired | string or null | A third-person note on a terminal status, such as why it closed. |
| external_refrequired | string or null | Your own reference, as you sent it. |
| borrowerrequired | object | |
| borrower.first_namerequired | string | |
| borrower.last_namerequired | string | |
| borrower.emailrequired | string or null | Null for a key without referrals.contact:read. |
| borrower.phonerequired | string or null | 10 digits.Null for a key without referrals.contact:read. |
| vehiclerequired | object | |
| vehicle.vinrequired | string or null | |
| vehicle.yearrequired | integer or null | |
| vehicle.makerequired | string or null | |
| vehicle.modelrequired | string or null | |
| claimrequired | object | |
| claim.carrierrequired | string or null | |
| claim.claim_numberrequired | string or null | |
| claim.loss_staterequired | string or null | |
| claim.date_of_lossrequired | string (date-time) or null | Midnight UTC on the date of loss. |
| claim.initial_offer_centsrequired | integer or null | |
| liabilityrequired | object | |
| liability.loan_payoff_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| liability.deductible_centsrequired | integer or null | |
| programrequired | object | |
| program.moderequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: provider_paid, split_pay, customer_paid, membership |
| program.subsidy_typerequired | string or null | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: percent, fixed_cents |
| program.subsidy_valuerequired | integer or null | |
| program.price_centsrequired | integer | Your price for this referral's consultation, in cents, fixed when the referral was submitted. mode and the subsidy terms decide how much of it you pay. |
| program.lockedrequired | boolean or null | True once the borrower activated under these terms; they can no longer change. Null once reporting is withdrawn (status: reporting_withdrawn). |
| consentrequired | object | |
| consent.moderequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: invitation, warm_handoff |
| consent.disclosure_confirmed_atrequired | string (date-time) or null | |
| consent.disclosure_channelrequired | string or null | |
| consent.disclosure_attestor_namerequired | string or null | Null for a key without referrals.contact:read. |
| submitted_viarequired | string | Where the referral came from: api, dashboard or csv. |
| created_atrequired | string (date-time) | |
| invited_atrequired | string (date-time) or null | |
| invitation_expires_atrequired | string (date-time) or null | |
| activated_atrequired | string (date-time) or null | |
| settled_atrequired | string (date-time) or null | |
| closed_atrequired | string (date-time) or null | |
| reporting_withdrawn_atrequired | string (date-time) or null | When the borrower withdrew permission to report this referral's progress to you; null while they haven't. From then on the referral shows status: reporting_withdrawn and no progress: its invited, expiry, activated, settled and closed times and program.locked are null. |
ReferralAcknowledgement
What a write answers a key that doesn't hold referrals:read: the referral's id and status, never its data. A key that holds referrals:read reads the referral itself.
Fields (3)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The referral's id. |
| objectrequired | "gap.referral" | |
| statusrequired | string | New values may be added: show one you don't recognise as not yet known, and don't fail.One of: submitted, invited, handoff_pending, outreach_queued, contact_attempted, activated, in_progress, settled, closed, declined, unreachable, expired, cancelled, reporting_withdrawn |
ReferralBulkAcknowledgement
Fields (9)
| Field | Type | Description |
|---|---|---|
| objectrequired | "bulk_result" | |
| createdrequired | integer | Rows created now (replays are not counted). |
| failedrequired | integer | |
| resultsrequired | array of object | |
| results[].indexrequired | integer | 0-based index into referrals. |
| results[].okrequired | boolean | |
| results[].referral | ReferralAcknowledgement | |
| results[].idempotent_replay | boolean | |
| results[].errors | BulkRowAcknowledgementErrorarray of BulkRowAcknowledgementError |
ReferralBulkInput
Fields (1)
| Field | Type | Description |
|---|---|---|
| referralsrequired | ReferralBulkRowarray of ReferralBulkRow | 1 to 500 referrals. Each row is validated and created on its own. |
ReferralBulkResult
Fields (9)
| Field | Type | Description |
|---|---|---|
| objectrequired | "bulk_result" | |
| createdrequired | integer | Rows created now (replays are not counted). |
| failedrequired | integer | |
| resultsrequired | array of object | |
| results[].indexrequired | integer | 0-based index into referrals. |
| results[].okrequired | boolean | |
| results[].referral | Referral | |
| results[].idempotent_replay | boolean | |
| results[].errors | BulkRowErrorarray of BulkRowError |
ReferralBulkRow
One referral in a bulk create.
Fields (28)
| Field | Type | Description |
|---|---|---|
| borrower_first_namerequired | string | |
| borrower_last_namerequired | string | |
| borrower_email | string or null | An email address of at most 254 characters, with no spaces, a single @, and a domain with a dot that has text before it and at least two characters after it (example.com). Spaces around it are ignored. Stored in lowercase. At least one of borrower_email and borrower_phone is required. An invitation referral (the default consent_mode) needs an email while text invitations are off for its loss state: one without it is refused with 422 email_required_sms_disabled. A warm-handoff referral may be phone-only. |
| borrower_phone | string or null | A 10-digit US number; punctuation and a leading 1 are tolerated, and it is stored as 10 digits. |
| consent_mode | string or null | invitation (the default): we send the borrower a co-branded activation link. warm_handoff: your staff delivered the disclosure script and the borrower agreed to be contacted; send the attestation as disclosure. Case-insensitive.One of: invitation, warm_handoff |
| vin | string or null | 11 to 17 letters and digits, never I, O or Q. Case-insensitive; stored in uppercase. |
| vehicle_year | integer or null | A model year from 1950 to two years after the current year. |
| vehicle_make | string or null | |
| vehicle_model | string or null | |
| primary_carrier | string or null | The borrower's insurer. |
| claim_number | string or null | The borrower's claim number with that insurer. |
| loss_state | string or null | The two-letter state of the loss, case-insensitive (tx is TX). |
| garaged_state | string or null | The two-letter USPS code of the state where the vehicle is garaged (a state or DC), case-insensitive. We judge this state when it is sent, else loss_state: a referral for a state we don't serve may be refused with 422 state_not_served. |
| date_of_loss | string (date) or null | The date of the loss; at most a day ahead (a timezone ahead of UTC). Spaces around it are ignored. |
| initial_offer_cents | integer or null | The insurer's initial ACV offer, in cents. |
| loan_payoff_cents | integer or null | The outstanding loan payoff, in cents. It drives your exposure figures and stays editable after activation. |
| deductible_cents | integer or null | The deductible, in cents. Stays editable after activation. |
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| program | ReferralProgramInputReferralProgramInput or null | Per-referral program terms. Omit, or send null, to use your program defaults. |
| disclosure | DisclosureInputDisclosureInput or null | Warm-handoff attestation, only with consent_mode: "warm_handoff". With it the referral is created ready for our outreach; without it the referral waits for your attestation. |
| claim_against | string or null | Whose insurer is handling the claim: own (the borrower's own insurer), other_driver (another driver's insurer), or unknown. Case-insensitive. Leave it out when you don't know.One of: own, other_driver, unknown |
| cause_of_loss | string or null | The cause of the loss, if you know it. weather is hail, wind or a storm; a flood is flood. Case-insensitive.One of: collision, theft, fire, flood, weather, vandalism, animal, other |
| trigger | string or null | What led you to refer: payoff_request (an insurer asked you for the loan payoff), loss_notice (a notice of loss), gap_claim (the borrower filed a GAP claim), borrower_request (the borrower asked), or other. Case-insensitive.One of: payoff_request, loss_notice, gap_claim, borrower_request, other |
| payoff_requested_at | string or null | When the insurer asked you for the loan payoff: a date (2026-06-16, read as midnight UTC) or an ISO 8601 timestamp with its UTC offset (2026-06-16T15:04:00-06:00). At most a day ahead. |
| payoff_request_channel | string or null | How that payoff request reached you. web_portal is your own online portal; electronic_service is a third-party payoff or letter-of-guarantee service the insurer used. Case-insensitive.One of: phone, fax, email, mail, web_portal, electronic_service, other |
| client_name | string or null | For a GAP administrator: the lender or dealer whose borrower this is. At most 120 characters. |
| requirement_basis | RequirementBasisInputRequirementBasisInput or null | Send it only when the borrower's contract lets you require the review. It is recorded and checked against your program's bases; messages to the borrower still ask rather than require. |
| idempotency_key | string or null | Makes this row safe to retry: a row with a key already used by your institution, in this key's mode, returns the original referral with idempotent_replay: true. A string of at most 255 characters; a longer key is refused, never cut. A live key can't send one that starts with test: (the row fails with idempotency_key_mode_conflict). |
ReferralCancelInput
Cancel the referral. Nothing else may be sent with it.
Fields (1)
| Field | Type | Description |
|---|---|---|
| actionrequired | "cancel" | Cancels the referral. Allowed only before the borrower activates (409 after). |
ReferralCreateAcknowledgement
Every field of ReferralAcknowledgement, and:
Fields (1)
| Field | Type | Description |
|---|---|---|
| idempotent_replayrequired | boolean | True when this answers a replay of this same request under its Idempotency-Key. |
ReferralCreateInput
One borrower referral. Rules across fields: at least one of borrower_email and borrower_phone; a split-pay program needs both subsidy terms; disclosure only with a warm handoff.
Fields (27)
| Field | Type | Description |
|---|---|---|
| borrower_first_namerequired | string | |
| borrower_last_namerequired | string | |
| borrower_email | string or null | An email address of at most 254 characters, with no spaces, a single @, and a domain with a dot that has text before it and at least two characters after it (example.com). Spaces around it are ignored. Stored in lowercase. At least one of borrower_email and borrower_phone is required. An invitation referral (the default consent_mode) needs an email while text invitations are off for its loss state: one without it is refused with 422 email_required_sms_disabled. A warm-handoff referral may be phone-only. |
| borrower_phone | string or null | A 10-digit US number; punctuation and a leading 1 are tolerated, and it is stored as 10 digits. |
| consent_mode | string or null | invitation (the default): we send the borrower a co-branded activation link. warm_handoff: your staff delivered the disclosure script and the borrower agreed to be contacted; send the attestation as disclosure. Case-insensitive.One of: invitation, warm_handoff |
| vin | string or null | 11 to 17 letters and digits, never I, O or Q. Case-insensitive; stored in uppercase. |
| vehicle_year | integer or null | A model year from 1950 to two years after the current year. |
| vehicle_make | string or null | |
| vehicle_model | string or null | |
| primary_carrier | string or null | The borrower's insurer. |
| claim_number | string or null | The borrower's claim number with that insurer. |
| loss_state | string or null | The two-letter state of the loss, case-insensitive (tx is TX). |
| garaged_state | string or null | The two-letter USPS code of the state where the vehicle is garaged (a state or DC), case-insensitive. We judge this state when it is sent, else loss_state: a referral for a state we don't serve may be refused with 422 state_not_served. |
| date_of_loss | string (date) or null | The date of the loss; at most a day ahead (a timezone ahead of UTC). Spaces around it are ignored. |
| initial_offer_cents | integer or null | The insurer's initial ACV offer, in cents. |
| loan_payoff_cents | integer or null | The outstanding loan payoff, in cents. It drives your exposure figures and stays editable after activation. |
| deductible_cents | integer or null | The deductible, in cents. Stays editable after activation. |
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| program | ReferralProgramInputReferralProgramInput or null | Per-referral program terms. Omit, or send null, to use your program defaults. |
| disclosure | DisclosureInputDisclosureInput or null | Warm-handoff attestation, only with consent_mode: "warm_handoff". With it the referral is created ready for our outreach; without it the referral waits for your attestation. |
| claim_against | string or null | Whose insurer is handling the claim: own (the borrower's own insurer), other_driver (another driver's insurer), or unknown. Case-insensitive. Leave it out when you don't know.One of: own, other_driver, unknown |
| cause_of_loss | string or null | The cause of the loss, if you know it. weather is hail, wind or a storm; a flood is flood. Case-insensitive.One of: collision, theft, fire, flood, weather, vandalism, animal, other |
| trigger | string or null | What led you to refer: payoff_request (an insurer asked you for the loan payoff), loss_notice (a notice of loss), gap_claim (the borrower filed a GAP claim), borrower_request (the borrower asked), or other. Case-insensitive.One of: payoff_request, loss_notice, gap_claim, borrower_request, other |
| payoff_requested_at | string or null | When the insurer asked you for the loan payoff: a date (2026-06-16, read as midnight UTC) or an ISO 8601 timestamp with its UTC offset (2026-06-16T15:04:00-06:00). At most a day ahead. |
| payoff_request_channel | string or null | How that payoff request reached you. web_portal is your own online portal; electronic_service is a third-party payoff or letter-of-guarantee service the insurer used. Case-insensitive.One of: phone, fax, email, mail, web_portal, electronic_service, other |
| client_name | string or null | For a GAP administrator: the lender or dealer whose borrower this is. At most 120 characters. |
| requirement_basis | RequirementBasisInputRequirementBasisInput or null | Send it only when the borrower's contract lets you require the review. It is recorded and checked against your program's bases; messages to the borrower still ask rather than require. |
ReferralCreateResult
Every field of Referral, and:
Fields (1)
| Field | Type | Description |
|---|---|---|
| idempotent_replayrequired | boolean | True when this answers a replayed Idempotency-Key with the original referral. |
ReferralDetail
A referral with the provider-facing milestone timeline and the financial panel (offers, settlement, exposure before and after, net savings, ROI).
Every field of Referral, and:
Fields (30)
| Field | Type | Description |
|---|---|---|
| consultation_numberrequired | string or null | Our consultation number, once the borrower activates. |
| days_in_negotiationrequired | integer or null | |
| timelinerequired | object or null | The provider-facing milestone timeline. Null once reporting is withdrawn (status: reporting_withdrawn). |
| timeline.current_milestonerequired | string or null | New values may be added: show one you don't recognise as not yet known, and don't fail.One of: referral_submitted, borrower_invited, disclosure_attested, outreach_in_progress, borrower_activated, valuation_report_received, insurance_details_received, photos_received, market_research, preliminary_estimate_ready, appraisal_report_complete, authorization_received, insurer_notified, insurer_acknowledged, insurer_appraiser_appointed, appraisals_exchanged, umpire_engaged, award_letter_issued, settlement_finalized, outcome_recorded |
| timeline.terminalrequired | object or null | |
| timeline.terminal.statusrequired | string | New values may be added: show one you don't recognise as not yet known, and don't fail.One of: submitted, invited, handoff_pending, outreach_queued, contact_attempted, activated, in_progress, settled, closed, declined, unreachable, expired, cancelled |
| timeline.terminal.labelrequired | string | |
| timeline.terminal.detailrequired | string | |
| timeline.phasesrequired | array of object | |
| timeline.phases[].keyrequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: referral, setup, research, negotiation, settlement |
| timeline.phases[].labelrequired | string | |
| timeline.phases[].staterequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: complete, current, upcoming, skipped |
| timeline.phases[].milestonesrequired | TimelineMilestonearray of TimelineMilestone | |
| financialrequired | object or null | The financial panel. Null once reporting is withdrawn (status: reporting_withdrawn). |
| financial.initial_offer_centsrequired | integer or null | |
| financial.current_best_offer_centsrequired | integer or null | |
| financial.appraised_value_centsrequired | integer or null | |
| financial.final_settlement_centsrequired | integer or null | |
| financial.loan_payoff_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| financial.deductible_centsrequired | integer or null | |
| financial.fees_paid_centsrequired | integer or null | |
| financial.gross_uplift_centsrequired | integer or null | |
| financial.exposure_before_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| financial.exposure_after_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| financial.exposure_reduction_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| financial.net_savings_centsrequired | integer or null | Null for a key without referrals.contact:read. |
| financial.roi_multiplerequired | number or null | Null for a key without referrals.contact:read. |
| financial.figures_are_finalrequired | boolean | |
| financial.savings_statementrequired | string or null | Null for a key without referrals.contact:read. |
| financial.payoff_missingrequired | boolean |
ReferralList
Fields (4)
| Field | Type | Description |
|---|---|---|
| objectrequired | "list" | |
| datarequired | Referralarray of Referral | |
| has_morerequired | boolean | |
| next_cursorrequired | string or null | Pass as starting_after for the next page; null on the last page. With updated_after it is never null: has_more says whether to fetch the next page now, and the last page's cursor is where your next poll resumes. |
ReferralPatchInput
Either { "action": "cancel" } alone, or field edits.
One of ReferralCancelInput or ReferralUpdateInput.
ReferralProgramEditInput
New program terms for a referral, as on create, except that mode and subsidy_type are matched as sent: a value with spaces around it is refused. A mode without subsidy terms inherits your default subsidy only when it is split-pay.
Fields (3)
| Field | Type | Description |
|---|---|---|
| mode | string | Who pays for the appraisal. Case-insensitive. Matched as sent: a value with spaces around it is refused.One of: provider_paid, split_pay, customer_paid |
| subsidy_type | string or null | Split-pay only: how subsidy_value reads. Case-insensitive. Matched as sent: a value with spaces around it is refused.One of: percent, fixed_cents |
| subsidy_value | integer or null | Split-pay only: the percent of the fee you cover (1 to 100), or the cents you cover, per subsidy_type. A split-pay referral needs both subsidy_type and a positive subsidy_value. |
ReferralProgramInput
Per-referral program terms, overriding your program defaults. A mode without subsidy terms inherits your default subsidy only when it is split-pay.
Fields (3)
| Field | Type | Description |
|---|---|---|
| mode | string | Who pays for the appraisal. Case-insensitive.One of: provider_paid, split_pay, customer_paid |
| subsidy_type | string or null | Split-pay only: how subsidy_value reads. Case-insensitive.One of: percent, fixed_cents |
| subsidy_value | integer or null | Split-pay only: the percent of the fee you cover (1 to 100), or the cents you cover, per subsidy_type. A split-pay referral needs both subsidy_type and a positive subsidy_value. |
ReferralSimulateInput
One simulated step for a sandbox referral (test keys only).
Fields (4)
| Field | Type | Description |
|---|---|---|
| torequired | string | The status to move the sandbox referral to; a step off its lifecycle is a 409 invalid_transition.One of: invited, activated, in_progress, settled, closed, declined, unreachable, expired |
| outcome | object | With to: "settled" only, and required there: the figures to settle with. The uplift and the exposure figures are computed from them as for a live settlement. |
| outcome.appraised_value_cents | integer | |
| outcome.final_settlement_centsrequired | integer |
ReferralUpdateInput
Edit a referral. loan_payoff_cents and deductible_cents stay editable after activation; every other field locks when the borrower activates (409 referral_locked). Null or "" clears an optional field. Only spaces clears vehicle_make, vehicle_model, primary_carrier, claim_number and external_ref too, and is refused for the cents, the year, the email, the phone, the VIN and the loss and garaged states. The borrower's names can't be cleared, and program: null changes nothing.
Fields (25)
| Field | Type | Description |
|---|---|---|
| loan_payoff_cents | integer or null | Editable any time. Null or "" clears it. |
| deductible_cents | integer or null | Editable any time. Null or "" clears it. |
| borrower_first_name | string | At most 80 characters, as on create. It can't be cleared. |
| borrower_last_name | string | At most 80 characters, as on create. It can't be cleared. |
| borrower_email | string or null | An email address of at most 254 characters, with no spaces, a single @, and a domain with a dot that has text before it and at least two characters after it (example.com). Spaces around it are ignored. Stored in lowercase. At least one of borrower_email and borrower_phone is required. An invitation referral (the default consent_mode) needs an email while text invitations are off for its loss state: one without it is refused with 422 email_required_sms_disabled. A warm-handoff referral may be phone-only. |
| borrower_phone | string or null | A 10-digit US number; punctuation and a leading 1 are tolerated, and it is stored as 10 digits. |
| vin | string or null | 11 to 17 letters and digits, never I, O or Q. Case-insensitive; stored in uppercase. |
| vehicle_year | integer or null | A model year from 1950 to two years after the current year. |
| vehicle_make | string or null | |
| vehicle_model | string or null | |
| primary_carrier | string or null | |
| claim_number | string or null | |
| loss_state | string or null | The two-letter state of the loss, case-insensitive (tx is TX). |
| garaged_state | string or null | The two-letter USPS code of the state where the vehicle is garaged (a state or DC), case-insensitive. We judge this state when it is sent, else loss_state: a referral for a state we don't serve may be refused with 422 state_not_served. Editable until the borrower activates; null or "" clears it. |
| external_ref | string or null | Your own opaque reference, such as a case or file ID from your system; echoed in responses and webhooks. Never a loan, account or policy number (a credit union member number is an account number). |
| initial_offer_cents | integer or null | The insurer's initial ACV offer, in cents. |
| program | ReferralProgramEditInputReferralProgramEditInput or null | New program terms. Locked once the borrower activates under them (409 economics_locked); switching into provider-paid or split-pay needs a billing method (403 billing_required). Null is the same as leaving program out: the terms stay as they are. |
| claim_against | string or null | Whose insurer is handling the claim: own (the borrower's own insurer), other_driver (another driver's insurer), or unknown. Case-insensitive. Leave it out when you don't know. Editable until the borrower activates; null or "" clears it.One of: own, other_driver, unknown |
| cause_of_loss | string or null | The cause of the loss, if you know it. weather is hail, wind or a storm; a flood is flood. Case-insensitive. Editable until the borrower activates; null or "" clears it.One of: collision, theft, fire, flood, weather, vandalism, animal, other |
| trigger | string or null | What led you to refer: payoff_request (an insurer asked you for the loan payoff), loss_notice (a notice of loss), gap_claim (the borrower filed a GAP claim), borrower_request (the borrower asked), or other. Case-insensitive. Editable until the borrower activates; null or "" clears it.One of: payoff_request, loss_notice, gap_claim, borrower_request, other |
| payoff_requested_at | string or null | When the insurer asked you for the loan payoff: a date (2026-06-16, read as midnight UTC) or an ISO 8601 timestamp with its UTC offset (2026-06-16T15:04:00-06:00). At most a day ahead. Editable until the borrower activates; null or "" clears it. |
| payoff_request_channel | string or null | How that payoff request reached you. web_portal is your own online portal; electronic_service is a third-party payoff or letter-of-guarantee service the insurer used. Case-insensitive. Editable until the borrower activates; null or "" clears it.One of: phone, fax, email, mail, web_portal, electronic_service, other |
| client_name | string or null | For a GAP administrator: the lender or dealer whose borrower this is. At most 120 characters. Editable until the borrower activates; null or "" clears it. |
| requirement_basis | RequirementBasisInputRequirementBasisInput or null | Send it only when the borrower's contract lets you require the review. It is recorded and checked against your program's bases; messages to the borrower still ask rather than require. Editable until the borrower activates; null or "" clears it. |
| action | null | Null is the same as leaving action out. To cancel, send { "action": "cancel" } on its own. |
RequirementBasisInput
The contract basis for requiring the review. Every member is required except filed_form_ref, which a carrier basis needs.
Fields (10)
| Field | Type | Description |
|---|---|---|
| basis_kindrequired | string | contract: a provision in the borrower's own contract. carrier: a GAP insurance policy's form, filed in the state, which needs filed_form_ref. Case-insensitive.One of: contract, carrier |
| contract_form_idrequired | string | The contract form's identifier, as the form names itself. 1 to 64 characters: letters, digits, spaces and . , _ - / ( ) # &. Spaces around it are ignored. |
| contract_form_versionrequired | string | The form's version or edition: 1 to 64 characters, in the same characters as contract_form_id. Spaces around it are ignored. |
| contract_daterequired | string (date) | The date of the borrower's contract (YYYY-MM-DD); at most a day ahead. Spaces around it are ignored. |
| product_typerequired | string | The product the contract is: a GAP waiver, GAP insurance, or a loan without GAP. Case-insensitive.One of: gap_waiver, gap_insurance, loan_without_gap |
| staterequired | string | The two-letter USPS code of the state whose law the contract follows (a state or DC), case-insensitive. |
| a2_acknowledgedrequired | boolean | Whether the borrower acknowledged the signing disclosure in its own box (the program paper's Appendix A-2). |
| adopts_x5required | boolean | Whether the contract adopts X.5 (no delay; no charges). |
| adopts_x6required | boolean | Whether the contract adopts X.6 (no worse off). |
| filed_form_ref | string or null | A carrier basis's filed form: its filing reference, as an identifier of at most 64 letters, digits, dots, hyphens and underscores. Never a URL. Spaces around it are ignored. |
ReviewRecord
One version of a referral's review record. Versions only grow: one is written each time the record changes, never edited afterwards, and cited by { id, version, hash }.
Fields (9)
| Field | Type | Description |
|---|---|---|
| objectrequired | "gap.review_record" | |
| idrequired | string | The version's id. |
| referral_idrequired | string | The referral's id. |
| versionrequired | integer | The version's number: 1 for the record's first version, and one more for each after it. |
| hashrequired | string | SHA-256, in lowercase hex, of bundle's canonical JSON (RFC 8785): hash the bundle you read to check it. A version never changes, and a redacted one keeps its hash. |
| statusrequired | string | The review's status as of this version. PENDING: referred, and the borrower hasn't engaged yet. IN_REVIEW: the borrower activated, and no review has reached them. REVIEWED_NO_UNDERVALUATION: we told the borrower the offer looks fair. REVIEWED_UNDERVALUATION_FOUND: we told the borrower we can help. RESEARCH_DELIVERED: our research reached the borrower, with no verdict. REVIEWED_ELSEWHERE: the borrower had the review done elsewhere. DECLINED: the borrower declined. UNREACHABLE: we couldn't reach the borrower, or the invitation expired. RELEASED: the review window ended with nothing delivered. CANCELLED: you cancelled the referral before the borrower activated. REPORTING_WITHDRAWN: the borrower asked us to stop reporting this referral's progress. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: PENDING, IN_REVIEW, REVIEWED_NO_UNDERVALUATION, REVIEWED_UNDERVALUATION_FOUND, RESEARCH_DELIVERED, REVIEWED_ELSEWHERE, DECLINED, UNREACHABLE, RELEASED, CANCELLED, REPORTING_WITHDRAWN |
| created_atrequired | string (date-time) | When the version was written. |
| redactedrequired | boolean | True once the version's bundle has been removed: when its retention period ends, or when the borrower's data is erased at their request. Its id, version, hash and status stay. |
| bundlerequired | ReviewRecordBundle or WithdrawnReviewRecordBundleReviewRecordBundle or WithdrawnReviewRecordBundle or null | The record itself, or null when the version is redacted. |
ReviewRecordBundle
One version of a referral's review record (review-record.v1). It names no one and carries no contact detail, VIN, claim number or dollar range, and it shows a verdict only once the verdict reached the borrower. Every date is ISO 8601 in UTC, and every amount integer cents.
Fields (67)
| Field | Type | Description |
|---|---|---|
| schemarequired | "review-record.v1" | The bundle's format. |
| referral_idrequired | string | The referral's id. |
| statusrequired | string | The review's status as of this version, as the record's status gives it. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: PENDING, IN_REVIEW, REVIEWED_NO_UNDERVALUATION, REVIEWED_UNDERVALUATION_FOUND, RESEARCH_DELIVERED, REVIEWED_ELSEWHERE, DECLINED, UNREACHABLE, RELEASED, CANCELLED |
| review_completed_atrequired | string (date-time) or null | When the review completed: when our verdict, or our paid research, first reached the borrower, or when they had the review done elsewhere. Null until then; nothing after it moves it. |
| verdictrequired | object | |
| verdict.recommendationrequired | string or null | The verdict that reached the borrower. PROCEED: we told them we can help. DECLINE: we told them the offer looks fair. Null until one reaches them: you never hear a verdict before the borrower does. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PROCEED, DECLINE |
| verdict.delivered_atrequired | string (date-time) or null | When that verdict reached the borrower. |
| verdict.tierrequired | string or null | The review the verdict came from. PRELIMINARY: made before the valuation report, photos and receipts were all in. FULL: made with all of them. Null without a verdict. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PRELIMINARY, FULL |
| verdict.decline_reasonrequired | string or null | With a DECLINE, why, when a reason was recorded: OFFER_FAIR, GAIN_BELOW_THRESHOLD, NO_APPRAISAL_RIGHT, EVIDENCE_INSUFFICIENT, OUT_OF_SCOPE or OTHER. Null otherwise. |
| verdict.research_onlyrequired | boolean | True when the borrower engaged us for research only, which carries no verdict. |
| verdict.research_delivered_atrequired | string (date-time) or null | When our paid research reached the borrower. |
| verdict.full_fac_required_atrequired | string (date-time) or null | When a full review became due: the borrower engaged us after a preliminary one. |
| verdict.full_fac_completed_atrequired | string (date-time) or null | When that full review was completed. |
| verdict.re_review_requested_atrequired | string (date-time) or null | When a change to the vehicle or the evidence sent the verdict back to be reviewed again. |
| materialityrequired | object | |
| materiality.rulerequired | string | The rule we_can_help follows: D12 today. Other rules may be added: treat one you don't recognise as a rule you don't know, and read we_can_help as that rule's verdict. |
| materiality.we_can_helprequired | boolean or null | Whether we can help, under that rule: true when our valuation clears the insurer's offer by enough to be worth an appraisal, false when it doesn't. Never a dollar figure. Null when it couldn't be judged (no confirmed offer, or no valid valuation range), and until a verdict reaches the borrower. |
| materiality.second_review_pendingrequired | boolean | True while a second reviewer still has to check the verdict. |
| windowrequired | object | |
| window.valuation_received_atrequired | string (date-time) or null | When the valuation report for the review came in. |
| window.ends_atrequired | string (date-time) or null | When the review window ends: 11:59:59 PM Denver time on the earlier of 5 business days after the valuation report and 15 business days after the borrower was first told about the review. Null while no window runs. |
| releaserequired | object | |
| release.released_atrequired | string (date-time) or null | When the borrower was released: the review window ended with nothing delivered. A review the borrower has started goes on. |
| release.notice_sent_atrequired | string (date-time) or null | When the release notice went to the borrower. |
| outreachrequired | object | |
| outreach.consent_moderequired | string | How the borrower came to us: INVITATION (we invited them) or WARM_HANDOFF (your staff handed them to us). New values may be added: treat one you don't recognise as unknown, and don't fail.One of: INVITATION, WARM_HANDOFF |
| outreach.invited_atrequired | string (date-time) or null | When we sent the invitation. |
| outreach.disclosure_confirmed_atrequired | string (date-time) or null | When your staff attested to the warm-handoff disclosure. |
| outreach.first_contact_atrequired | string (date-time) or null | When we first contacted the borrower. |
| outreach.contact_attemptsrequired | integer | How many times we tried to reach the borrower. |
| outreach.last_contact_attempt_atrequired | string (date-time) or null | When we last tried. |
| outreach.reached_atrequired | string (date-time) or null | When we reached the borrower. |
| outreach.activated_atrequired | string (date-time) or null | When the borrower activated. |
| outreach.contact_stopped_atrequired | string (date-time) or null | When the borrower asked us to stop contacting them about this referral. |
| refusalrequired | object | |
| refusal.recorded_atrequired | string (date-time) or null | When the borrower's "no thanks" was recorded. |
| refusal.scoperequired | string or null | What the refusal covers, as a code. |
| refusal.methodrequired | string or null | How the borrower gave it, as a code. |
| refusal.text_versionrequired | string or null | The version of the refusal text the borrower heard. |
| reviewed_elsewhere_atrequired | string (date-time) or null | When we recorded that the borrower had the review done elsewhere. |
| appraisal_rightrequired | object | |
| appraisal_right.claim_againstrequired | string or null | Whose insurer is handling the claim: own, other_driver or unknown, as you or the borrower told us. |
| appraisal_right.clause_invoked_atrequired | string (date-time) or null | When the policy's appraisal clause was invoked with the insurer. |
| appraisal_right.clause_invoked_viarequired | string or null | How it was invoked, as a code. |
| costsrequired | object | |
| costs.engagementrequired | object or null | The engagement fee: who pays it, and each part. Null for a key without reporting:read.Null for a key without reporting:read. |
| costs.engagement.basisrequired | string | SNAPSHOT: the funding terms fixed when the borrower activated. LEGACY: the program's original prices, for a referral without them. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: SNAPSHOT, LEGACY |
| costs.engagement.payerrequired | string | Who pays the engagement fee: PARTNER (you), BORROWER, SPLIT (both), MEMBERSHIP (a membership covers it) or NONE (there is no fee). New values may be added: treat one you don't recognise as unknown, and don't fail.One of: PARTNER, BORROWER, SPLIT, MEMBERSHIP, NONE |
| costs.engagement.list_centsrequired | integer | The engagement fee before anyone's part, in cents. |
| costs.engagement.institution_centsrequired | integer | Your part of it, in cents. |
| costs.engagement.borrower_centsrequired | integer | What the borrower is charged for it, in cents, after any discount. |
| costs.institution_chargesrequired | array of object or null | What we have charged you on this referral, summed by kind and status. Null for a key without reporting:read.Null for a key without reporting:read. |
| costs.institution_charges[].kindrequired | string | The charges' kind. |
| costs.institution_charges[].statusrequired | string | Their status. |
| costs.institution_charges[].amount_centsrequired | integer | Their total, in cents. |
| costs.institution_charges[].countrequired | integer | How many there are. |
| requirementrequired | object | |
| requirement.moderequired | string | The review requirement your program had when the borrower activated: REQUEST (also when none was set), REVIEW or REVIEW_AND_APPRAISAL. Messages to the borrower ask rather than require. |
| requirement.basis_attestedrequired | boolean | Whether you sent a requirement_basis with the referral. |
| requirement.basis_validated_atrequired | string (date-time) or null | When that basis matched one of your program's approved bases. |
| outcomerequired | object | |
| outcome.settled_atrequired | string (date-time) or null | When the borrower's claim settled. |
| outcome.closed_atrequired | string (date-time) or null | When the referral's record closed. |
| outcome.close_reasonrequired | string or null | Why it closed: null while it is open. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: SETTLED, NO_MATERIAL_UNDERVALUATION, BORROWER_DECLINED, BORROWER_UNREACHABLE, INVITATION_EXPIRED, CANCELLED_BY_INSTITUTION, REVIEWED_ELSEWHERE, SETTLED_WITH_INSURER, OUT_OF_SCOPE, STATE_NOT_SERVED, NO_APPRAISAL_RIGHT, CLAIM_NOT_COVERED, NOT_COMPLETED, REPORTING_WITHDRAWN |
| copy_versionsrequired | object | |
| copy_versions.disclosure_scriptrequired | string or null | The version of the warm-handoff disclosure script your staff delivered. |
| copy_versions.refusal_textrequired | string or null | The version of the refusal text the borrower heard. |
ReviewRecordReference
The latest version of the referral's review record, cited as the review webhooks cite one.
Fields (5)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The version's id. |
| versionrequired | integer | The version's number: 1 for the record's first version, and one more for each after it. |
| hashrequired | string | SHA-256, in lowercase hex, of the version's canonical JSON (RFC 8785). A version never changes, so the version you read later still matches it. |
| statusrequired | string | The review's status as of this version. PENDING: referred, and the borrower hasn't engaged yet. IN_REVIEW: the borrower activated, and no review has reached them. REVIEWED_NO_UNDERVALUATION: we told the borrower the offer looks fair. REVIEWED_UNDERVALUATION_FOUND: we told the borrower we can help. RESEARCH_DELIVERED: our research reached the borrower, with no verdict. REVIEWED_ELSEWHERE: the borrower had the review done elsewhere. DECLINED: the borrower declined. UNREACHABLE: we couldn't reach the borrower, or the invitation expired. RELEASED: the review window ended with nothing delivered. CANCELLED: you cancelled the referral before the borrower activated. REPORTING_WITHDRAWN: the borrower asked us to stop reporting this referral's progress. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: PENDING, IN_REVIEW, REVIEWED_NO_UNDERVALUATION, REVIEWED_UNDERVALUATION_FOUND, RESEARCH_DELIVERED, REVIEWED_ELSEWHERE, DECLINED, UNREACHABLE, RELEASED, CANCELLED, REPORTING_WITHDRAWN |
| created_atrequired | string (date-time) | When the version was written. |
ReviewStatus
One of your referrals the lookup matched, and its review status: the version its record stands at.
Fields (6)
| Field | Type | Description |
|---|---|---|
| objectrequired | "gap.review_status" | |
| referralrequired | object | |
| referral.idrequired | string | The referral's id. |
| referral.external_refrequired | string or null | Your own reference, as you sent it. |
| recordrequired | ReviewRecordReferenceReviewRecordReference or null | The latest version of the referral's review record. Null while it has none to show: before its first version is written, and from the borrower's request that we stop reporting the referral's progress until the version that records it. |
| close_reasonrequired | string or null | Why the referral's record closed, as of that version: null while it is open, and whenever record is null. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: SETTLED, NO_MATERIAL_UNDERVALUATION, BORROWER_DECLINED, BORROWER_UNREACHABLE, INVITATION_EXPIRED, CANCELLED_BY_INSTITUTION, REVIEWED_ELSEWHERE, SETTLED_WITH_INSURER, OUT_OF_SCOPE, STATE_NOT_SERVED, NO_APPRAISAL_RIGHT, CLAIM_NOT_COVERED, NOT_COMPLETED, REPORTING_WITHDRAWN |
ReviewStatusList
Your referrals the identifier matched, newest first, at most 100; an empty list when none did.
Fields (3)
| Field | Type | Description |
|---|---|---|
| objectrequired | "list" | |
| datarequired | ReviewStatusarray of ReviewStatus | |
| has_morerequired | boolean | True when more than 100 of your referrals matched: the newest 100 are listed. |
ReviewStatusLookupInput
Exactly one identifier, matched against your referrals: vin, or claim_number or external_ref as you sent it on the referral, spaces around it aside. One that is blank or null is not sent.
Fields (3)
| Field | Type | Description |
|---|---|---|
| vin | string or null | The vehicle's VIN: 11 to 17 letters and digits, never I, O or Q. Case-insensitive. |
| claim_number | string or null | The borrower's claim number with their insurer, as you sent it on the referral. |
| external_ref | string or null | Your own reference for the referral, as you sent it. |
SignatureCheckResult
What a test credential's signed request looked like to the API: whether it verified, and the base it was checked over.
Fields (4)
| Field | Type | Description |
|---|---|---|
| objectrequired | "gap.signature_check" | |
| verifiedrequired | boolean | Whether the request's signature verified with the credential's key over the signature base built from the request as received. |
| signature_base | string | The signature base built from the request as received (RFC 9421 section 2.5), to compare byte for byte with the one you signed. |
| code | string | When verified is false: the code the same request would be refused with on any other operation. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: signature_profile_invalid, signature_expired, target_uri_not_allowed, signature_invalid, environment_mismatch |
ThinEvent
One event, thin: what happened, to which record, and when. It is what the events feed lists and what a thin-payload webhook endpoint is sent (one created from the partner security platform's issue stage on), with the webhook's headers. New event types are added over time: a feed reader skips a type it doesn't recognise, and a webhook endpoint answers 2xx to a type it doesn't recognise, and ignores it.
Fields (9)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The event id (evt_…), the same id its webhook delivery carries: use it to drop duplicates across the feed and your webhooks. It is not a position: page with next_cursor. |
| objectrequired | "event" | |
| typerequired | string | New values may be added: ignore a type you don't recognise.One of: referral.invited, referral.activated, referral.declined, consultation.status_changed, appraisal.completed, settlement.updated, charge.created, plan.enrollment.activated, plan.enrollment.past_due, plan.enrollment.cancelled, plan.benefit.redeemed, plan.benefit.completed, plan.statement.issued, review.completed, review.updated, referral.closed |
| api_versionrequired | "1.1" | The thin event's version: 1.1. |
| created_atrequired | string (date-time) | |
| livemode | boolean | true for a live event, false for a sandbox one; a membership plan event has its enrollment's. A membership statement and its charge don't carry it: a live key lists them, and they go to your live endpoints. |
| accountrequired | object | The institution the event belongs to. |
| account.idrequired | string | Your institution's id. |
| datarequired | object | The thin body, by type: the referral's id, status, your external_ref and its timestamps under referral, or the enrollment's under enrollment; the milestone's key, phase, previous and completed list; the codes and ids the event adds (channel, via, consultation_number, reason, redemption_id); a charge's id, status and statement_id; a statement's id, period, status and times; a review event's record (id, version, hash, status) and close_reason. Never a borrower's contact details, a payoff, a VIN or an amount: fetch the referral or enrollment for those. On the events feed, a key without referrals:read gets the referral as its id only, so a charge.created it lists names its referral but not your reference, its status or its timestamps; a thin-payload webhook endpoint is sent the whole thin body. After an erasure an event carries only the id and scrubbed: true. |
TimelineMilestone
Fields (9)
| Field | Type | Description |
|---|---|---|
| keyrequired | string | New values may be added: show one you don't recognise as not yet known, and don't fail.One of: referral_submitted, borrower_invited, disclosure_attested, outreach_in_progress, borrower_activated, valuation_report_received, insurance_details_received, photos_received, market_research, preliminary_estimate_ready, appraisal_report_complete, authorization_received, insurer_notified, insurer_acknowledged, insurer_appraiser_appointed, appraisals_exchanged, umpire_engaged, award_letter_issued, settlement_finalized, outcome_recorded |
| labelrequired | string | |
| detailrequired | string | |
| staterequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: complete, current, upcoming, skipped |
| atrequired | string (date-time) or null | When the milestone completed, when known. |
| waiting_onrequired | string or null | Who the current milestone waits on. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: borrower, secondappraisal, insurer |
| waiting_labelrequired | string or null | |
| delayedrequired | boolean | True when the current milestone has run past its expected time. |
| noterequired | string or null | A third-person fact line, such as the number of contact attempts. |
WebhookEvent
The full payload: the envelope a full-payload endpoint, one created before the partner security platform's issue stage, is sent; a thin-payload endpoint, created from issue on, is sent ThinEvent instead, with the same headers. Every delivery is a POST. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise. Verify X-SecondAppraisal-Signature: t=<unix seconds>,v1=<hex> first: v1 is HMAC-SHA256 with your endpoint's signing secret over <t>.<raw body>; compare in constant time and refuse an old t. The key is the secret's text for a whsec_gap_ secret, and the base64-decoded bytes after whsec_ for a Standard Webhooks secret, whose endpoint also gets webhook-id, webhook-timestamp and webhook-signature, which any Standard Webhooks library verifies. Answer any 2xx to acknowledge; anything else is retried with backoff. New event types are added over time: answer 2xx to a type you don't recognise, and ignore it.
Fields (6)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The event id (evt_…). A retried delivery repeats it: use it to drop duplicates. |
| objectrequired | "event" | |
| typerequired | string | New values may be added: ignore a type you don't recognise.One of: referral.invited, referral.activated, referral.declined, consultation.status_changed, appraisal.completed, settlement.updated, charge.created, plan.enrollment.activated, plan.enrollment.past_due, plan.enrollment.cancelled, plan.benefit.redeemed, plan.benefit.completed, plan.statement.issued, review.completed, review.updated, referral.closed |
| created_atrequired | string (date-time) | |
| livemode | boolean | true for a live event, false for a sandbox one (from a test key's referral). Live events go to your live endpoints only, and sandbox referral events to your test endpoints only. The membership plan events don't carry it yet. |
| datarequired | object | Depends on type. |
WebhookReferral
A referral as a webhook carries it. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise. The API's referral carries more: fetch it to read every field. Once the borrower withdraws permission to report the referral's progress, it carries status: reporting_withdrawn and no progress, as every read shows it.
Fields (44)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The referral's id. |
| objectrequired | "gap.referral" | |
| statusrequired | string | Where the referral stands. reporting_withdrawn once the borrower withdrew permission to report its progress to you, whatever happens to it after: from then on it shows the fields you supplied and no progress. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: submitted, invited, handoff_pending, outreach_queued, contact_attempted, activated, in_progress, settled, closed, declined, unreachable, expired, cancelled, reporting_withdrawn |
| status_labelrequired | string | The status as the portal shows it. |
| status_tonerequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: green, yellow, red, neutral |
| status_noterequired | string or null | A third-person note on a terminal status, such as why it closed. |
| external_refrequired | string or null | Your own reference, as you sent it. |
| borrowerrequired | object | |
| borrower.first_namerequired | string | |
| borrower.last_namerequired | string | |
| borrower.emailrequired | string or null | |
| borrower.phonerequired | string or null | 10 digits. |
| vehiclerequired | object | |
| vehicle.vinrequired | string or null | |
| vehicle.yearrequired | integer or null | |
| vehicle.makerequired | string or null | |
| vehicle.modelrequired | string or null | |
| claimrequired | object | |
| claim.carrierrequired | string or null | |
| claim.claim_numberrequired | string or null | |
| claim.loss_staterequired | string or null | |
| claim.date_of_lossrequired | string (date-time) or null | Midnight UTC on the date of loss. |
| claim.initial_offer_centsrequired | integer or null | |
| liabilityrequired | object | |
| liability.loan_payoff_centsrequired | integer or null | |
| liability.deductible_centsrequired | integer or null | |
| programrequired | object | |
| program.moderequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: provider_paid, split_pay, customer_paid, membership |
| program.subsidy_typerequired | string or null | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: percent, fixed_cents |
| program.subsidy_valuerequired | integer or null | |
| program.price_centsrequired | integer | Your price for this referral's consultation, in cents, fixed when the referral was submitted. mode and the subsidy terms decide how much of it you pay. |
| program.lockedrequired | boolean or null | True once the borrower activated under these terms; they can no longer change. Null once reporting is withdrawn (status: reporting_withdrawn). |
| consentrequired | object | |
| consent.moderequired | string | New values may be added: treat one you don't recognise as unknown, and don't fail.One of: invitation, warm_handoff |
| consent.disclosure_confirmed_atrequired | string (date-time) or null | |
| consent.disclosure_channelrequired | string or null | |
| consent.disclosure_attestor_namerequired | string or null | |
| submitted_viarequired | string | Where the referral came from: api, dashboard or csv. |
| created_atrequired | string (date-time) | |
| invited_atrequired | string (date-time) or null | |
| invitation_expires_atrequired | string (date-time) or null | |
| activated_atrequired | string (date-time) or null | |
| settled_atrequired | string (date-time) or null | |
| closed_atrequired | string (date-time) or null |
WebhookReferralActivatedData
The referral, its consultation number, and how the borrower activated. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise.
Fields (3)
| Field | Type | Description |
|---|---|---|
| referralrequired | WebhookReferral | |
| consultation_numberrequired | string or null | Our consultation number for the borrower's case. |
| viarequired | string | Whether the borrower activated through the link or with our team's help. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: borrower_link, admin_assisted |
WebhookReferralData
The referral, as webhooks carry it. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise.
Fields (1)
| Field | Type | Description |
|---|---|---|
| referralrequired | WebhookReferral |
WebhookReferralInvitedData
The referral, and how its invitation went out. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise.
Fields (2)
| Field | Type | Description |
|---|---|---|
| referralrequired | WebhookReferral | |
| channelrequired | string | How the invitation went out. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: email, sms |
WebhookReviewRecord
The version of the referral's review record that the event reports.
Fields (4)
| Field | Type | Description |
|---|---|---|
| idrequired | string | The version's id. |
| versionrequired | integer | The version's number: 1 for the record's first version, and one more for each after it. |
| hashrequired | string | SHA-256, in lowercase hex, of the version's canonical JSON (RFC 8785). A version never changes, so the version you read later still matches it. |
| statusrequired | string | The review's status as of this version. PENDING: referred, and the borrower hasn't engaged yet. IN_REVIEW: the borrower activated, and no review has reached them. REVIEWED_NO_UNDERVALUATION: we told the borrower the offer looks fair. REVIEWED_UNDERVALUATION_FOUND: we told the borrower we can help. RESEARCH_DELIVERED: our research reached the borrower, with no verdict. REVIEWED_ELSEWHERE: the borrower had the review done elsewhere. DECLINED: the borrower declined. UNREACHABLE: we couldn't reach the borrower, or the invitation expired. RELEASED: the review window ended with nothing delivered. CANCELLED: you cancelled the referral before the borrower activated. REPORTING_WITHDRAWN: the borrower asked us to stop reporting this referral's progress. New values may be added: show one you don't recognise as not yet known, and don't fail.One of: PENDING, IN_REVIEW, REVIEWED_NO_UNDERVALUATION, REVIEWED_UNDERVALUATION_FOUND, RESEARCH_DELIVERED, REVIEWED_ELSEWHERE, DECLINED, UNREACHABLE, RELEASED, CANCELLED, REPORTING_WITHDRAWN |
WebhookReviewRecordData
The referral, and the version of its review record that the event reports. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise. A version that completes the review and closes the referral sends both review.completed and referral.closed. Staged: none of these events is sent until review records open.
Fields (5)
| Field | Type | Description |
|---|---|---|
| referralrequired | object | |
| referral.idrequired | string | The referral's id. |
| referral.external_refrequired | string or null | Your own reference, as you sent it. |
| recordrequired | WebhookReviewRecord | |
| close_reasonrequired | string or null | Why the referral's record closed: null while it is open, and again if it reopens. New values may be added: treat one you don't recognise as unknown, and don't fail.One of: SETTLED, NO_MATERIAL_UNDERVALUATION, BORROWER_DECLINED, BORROWER_UNREACHABLE, INVITATION_EXPIRED, CANCELLED_BY_INSTITUTION, REVIEWED_ELSEWHERE, SETTLED_WITH_INSURER, OUT_OF_SCOPE, STATE_NOT_SERVED, NO_APPRAISAL_RIGHT, CLAIM_NOT_COVERED, NOT_COMPLETED, REPORTING_WITHDRAWN |
WebhookSettlementData
The referral, and the outcome its settlement recorded. This object and each object in it carry at least these fields; new fields may be added, so ignore any you don't recognise.
Fields (10)
| Field | Type | Description |
|---|---|---|
| referralrequired | WebhookReferral | |
| outcomerequired | object | |
| outcome.initial_acv_centsrequired | integer or null | |
| outcome.appraised_value_centsrequired | integer or null | |
| outcome.final_settlement_centsrequired | integer or null | |
| outcome.uplift_centsrequired | integer or null | |
| outcome.exposure_before_centsrequired | integer or null | |
| outcome.exposure_after_centsrequired | integer or null | |
| outcome.fees_paid_centsrequired | integer or null | |
| outcome.settled_atrequired | string (date-time) or null |
WithdrawnReviewRecordBundle
A version written after the borrower asked us to stop reporting the referral's progress: it says only that, and when. The versions before it stay readable by number.
Fields (5)
| Field | Type | Description |
|---|---|---|
| schemarequired | "review-record.v1" | The bundle's format. |
| referral_idrequired | string | The referral's id. |
| statusrequired | "REPORTING_WITHDRAWN" | |
| close_reasonrequired | "REPORTING_WITHDRAWN" | |
| reporting_withdrawn_atrequired | string (date-time) | When the borrower asked us to stop reporting this referral's progress to you. |
Data classes
Each operation states the most sensitive class of data it carries.
- public
- A value with no meaning outside the API: a status name, a label, a count, an object type, or a timestamp of a public event.
- institution_confidential
- The institution's own business records with SecondAppraisal: its references, record ids, program terms, prices and fees, statements and aggregate reports.
- borrower_personal
- Facts about one borrower or member: their name, vehicle and VIN, insurance carrier and claim number, loss state and date, and the amounts of their loss, loan and settlement.
- borrower_contact
- How to reach one borrower or member: an email address or a phone number.
- credential
- A value that grants access by itself, such as a member's activation link.
curl https://secondappraisal.com/api/gap/v1/openapi