Quickstart: PHP
Get a prequalify decision from a fresh PHP project in under five minutes.
PHP Quickstart
Get a prequalify decision from a fresh PHP project in under five minutes. PHP 8.2+, PSR-18 transport, readonly value objects, constructor promotion, named arguments — and one flat $result->plans list on the way out.
1. Install
composer require isa-sdk/sdkThe SDK targets PHP 8.2+ and requires a PSR-18 HTTP client (guzzlehttp/guzzle and symfony/http-client both work out of the box). The Composer package is isa-sdk/sdk; the PHP namespace is Isa\Sdk.
2. Hello world
Set the bearer token and run prequalify:
export ISA_TOKEN=<YOUR_ISA_TOKEN><?php
use Isa\Sdk\Isa;
use Isa\Sdk\Zyins\Applicant;
use Isa\Sdk\Zyins\Coverage;
use Isa\Sdk\Zyins\Height;
use Isa\Sdk\Zyins\NicotineDuration;
use Isa\Sdk\Zyins\NicotineUsageInput;
use Isa\Sdk\Zyins\Reference\PrequalifyRequest;
use Isa\Sdk\Zyins\Sex;
use Isa\Sdk\Zyins\Weight;
use Isa\Sdk\Catalog\Products;
$isa = Isa::withBearer();
$result = $isa->zyins->prequalify->run(new PrequalifyRequest(
applicant: new Applicant(
dob: '1962-04-18',
sex: Sex::Male,
height: Height::fromFeetInches(5, 10),
weight: Weight::fromPounds(195),
state: 'NC',
nicotineUse: new NicotineUsageInput(lastUsed: NicotineDuration::Never),
),
coverage: Coverage::faceValue(25000),
products: [Products::fex()->aetnaAccendo()],
));
// $result->plans is a flat list — one entry per product.
foreach ($result->plans as $offer) {
foreach ($offer->pricing as $row) {
if ($row->primary && $row->premium !== null) {
echo $offer->carrier->name, ' ', $offer->product->name, ' ', $row->premium->amount->display, PHP_EOL;
}
}
}Isa::withBearer() reads ISA_TOKEN from the environment. No options, no config, no transport setup. Run it. The expected output is one line per qualifying offer:
Aetna Accendo Aetna Accendo Final Expense $87.42Money inputs are integer dollars.
Coverage::faceValue(25000)means $25,000 of death benefit, not 25,000 cents. Outputs always carry bothcents(integer minor units) anddisplay(the verbatim carrier string), so you never have to decide formatting on the way out.
3. A real example
A full prequalify + quote sequence for the canonical applicant: John Doe, born 1962-04-18, North Carolina, 5'10", 195 lb, no medications, against Aetna Accendo final expense at $25,000 of face value.
<?php
use Isa\Sdk\Isa;
use Isa\Sdk\Zyins\Applicant;
use Isa\Sdk\Zyins\Coverage;
use Isa\Sdk\Zyins\Height;
use Isa\Sdk\Zyins\NicotineDuration;
use Isa\Sdk\Zyins\NicotineUsageInput;
use Isa\Sdk\Zyins\Reference\PrequalifyRequest;
use Isa\Sdk\Zyins\Reference\QuoteRequest;
use Isa\Sdk\Zyins\Sex;
use Isa\Sdk\Zyins\Weight;
use Isa\Sdk\Catalog\Products;
$isa = Isa::withBearer();
$john = new Applicant(
dob: '1962-04-18',
sex: Sex::Male,
height: Height::fromFeetInches(5, 10),
weight: Weight::fromPounds(195),
state: 'NC',
nicotineUse: new NicotineUsageInput(lastUsed: NicotineDuration::Never),
);
$products = [Products::fex()->aetnaAccendo()];
// 1. Prequalify — best offer per product, with the uniform pricing[] table.
$prequalify = $isa->zyins->prequalify->run(new PrequalifyRequest(
applicant: $john,
coverage: Coverage::faceValue(25000),
products: $products,
));
foreach ($prequalify->plans as $offer) {
// deathBenefit is null for premium-only products (medsup); null-check it.
if ($offer->deathBenefit !== null) {
echo $offer->deathBenefit->amount->display, PHP_EOL; // "$25,000.00"
}
// Eligibility and premium live on each pricing[] row, not the offer.
foreach ($offer->pricing as $row) {
if ($row->primary && $row->premium !== null) {
echo $offer->carrier->name, ' ',
$row->rateClass, ' ', // 'graded', 'immediate', ...
$row->premium->amount->display, ' ', // verbatim carrier string
$row->premium->amount->cents, PHP_EOL; // integer cents
}
}
// Alternate qualifying tiers are sibling rows in $offer->pricing.
foreach ($offer->pricing as $row) {
if ($row->primary || !$row->eligibility->eligible || $row->premium === null) {
continue;
}
echo ' also: ', $row->rateClass, ' ', $row->premium->amount->display, PHP_EOL;
}
}
// 2. Quote — same flat plans[] shape, broader product set.
$quote = $isa->zyins->quote->run(new QuoteRequest(
applicant: $john,
coverage: Coverage::faceValue(25000),
products: $products,
));
echo 'quote returned ', count($quote->plans), ' plan(s)', PHP_EOL;Named arguments make the call sites self-documenting. Every input is a readonly class — you can't accidentally mutate an applicant mid-pipeline.
Both PrequalifyResult and QuoteResult are flat: $result->plans, $result->requestId, $result->idempotencyKey, $result->livemode. Persist requestId and idempotencyKey alongside your business records.
4. Multi-amount face-value quote
Request several coverage amounts in one call with Coverage::faceValues([…]). The response is always a flat $result->plans — the same product appears once per requested amount. Group client-side with PrequalifyResult::byAmount(...), keyed by deathBenefit->amount->cents.
<?php
use Isa\Sdk\Isa;
use Isa\Sdk\Zyins\Applicant;
use Isa\Sdk\Zyins\Coverage;
use Isa\Sdk\Zyins\Height;
use Isa\Sdk\Zyins\NicotineDuration;
use Isa\Sdk\Zyins\NicotineUsageInput;
use Isa\Sdk\Zyins\Reference\PrequalifyRequest;
use Isa\Sdk\Zyins\Reference\PrequalifyResult;
use Isa\Sdk\Zyins\Sex;
use Isa\Sdk\Zyins\Weight;
use Isa\Sdk\Catalog\Products;
$isa = Isa::withBearer();
$result = $isa->zyins->prequalify->run(new PrequalifyRequest(
applicant: new Applicant(
dob: '1962-04-18',
sex: Sex::Male,
height: Height::fromFeetInches(5, 10),
weight: Weight::fromPounds(195),
state: 'NC',
nicotineUse: new NicotineUsageInput(lastUsed: NicotineDuration::Never),
),
coverage: Coverage::faceValues([10000, 25000, 50000]),
products: [Products::term()->fidelityLifeInstabrainTerm()],
));
// Flat plans, grouped client-side by deathBenefit->amount->cents.
foreach (PrequalifyResult::byAmount($result->plans) as $cents => $offers) {
echo 'Amount ', $offers[0]->deathBenefit->amount->display, " ($cents c):", PHP_EOL;
foreach ($offers as $offer) {
foreach ($offer->pricing as $row) {
if ($row->primary && $row->premium !== null) {
echo ' ', $offer->carrier->name, ': ', $row->premium->amount->display, PHP_EOL;
}
}
}
}5. Errors
Every SDK exception inherits from Isa\Sdk\Zyins\Exception\IsaException. Catch on the specific subclass — don't strpos($e->getMessage(), ...):
<?php
use Isa\Sdk\Zyins\Exception\IsaException;
use Isa\Sdk\Zyins\Exception\IsaConfigException;
use Isa\Sdk\Zyins\Exception\IsaIdempotencyConflictException;
use Isa\Sdk\Zyins\Exception\IsaRateLimitException;
use Isa\Sdk\Zyins\Exception\IsaValidationException;
try {
$result = $isa->zyins->prequalify->run($request);
} catch (IsaConfigException $e) {
// Thrown synchronously when ISA_TOKEN is missing at construction.
$logger->error('configuration error', ['message' => $e->getMessage()]);
throw $e;
} catch (IsaIdempotencyConflictException $e) {
// Replayed key with a different body. Almost always a bug — log and bail.
$logger->error('idempotency conflict', [
'key' => $e->getKey(),
'first_seen' => $e->getFirstSeenAt(),
]);
throw $e;
} catch (IsaValidationException $e) {
// 400 from the API. $e->details() carries per-field details.
foreach ($e->details() as $field => $message) {
$logger->error('field error', ['field' => $field, 'message' => $message]);
}
throw $e;
} catch (IsaRateLimitException $e) {
// 429. Honor Retry-After if present.
sleep($e->retryAfterSeconds() ?? 1);
} catch (IsaException $e) {
// Any other 4xx / 5xx. Match on $e->code() (stable enum).
$logger->error('api error', ['code' => $e->code(), 'request_id' => $e->requestId()]);
throw $e;
}The five you see first:
| Exception | When it fires | What to do |
|---|---|---|
IsaConfigException | Isa::withBearer() called with no token and no ISA_TOKEN | Set the env var or pass the token explicitly |
IsaValidationException | 400 from the API | Inspect details(); fix the input |
IsaException | Any other 4xx/5xx | Match on the stable code enum |
IsaIdempotencyConflictException | Same Idempotency-Key, different body | Treat as a bug |
IsaRateLimitException | 429 | Sleep retryAfterSeconds(), retry |
6. Configuration
The defaults match what most consumers need:
| Default | Value | When to override |
|---|---|---|
| Base URL | Production | Set ISA_BASE_URL for staging or self-hosted |
| Timeout | 30s per request | Pass a RequestOptions(timeout: ...) to a specific call |
| Retries | 3 attempts, exponential backoff, 5xx + 429 only | RequestOptions(retries: 0) to disable per-call |
Idempotency-Key | UUID v4 minted per call | RequestOptions::default()->withIdempotencyKey(...) for cross-process replays |
| Transport | PSR-18 auto-discovered | Inject your own for proxies, mocking, telemetry |
If you find yourself overriding more than one of these in normal usage, file an issue — the default is probably wrong.
7. What's next
- Prequalify guide — full envelope, eligibility categories, premium mode grid, and field contract
- Custom sorting and grouping — override the default order, group by your own dimension
- Authentication guide — bearer tokens, test vs. live
- Idempotency guide — how the SDK mints keys, when to bring your own
- Errors reference — every
codevalue, status mapping, remediation - Webhooks guide — verifying signed webhook deliveries with
hash_equals - API Reference — every endpoint, every parameter, every response field
Need raw HTTP details (status, headers, request ID)? Every result carries requestId and idempotencyKey directly — the same provenance the response headers do.
Updated about 12 hours ago