Resources

Enrollment

Convert a quote into a real HMO policy. Pierflow verifies identity, computes settlement splits, issues the policy through the HMO connector, and ships a settlement instruction back so your ledger can credit each party.

Enrollment is the binding action: the user is committing to a plan, money is about to move, and the HMO is about to take them on as a member. Treat the call as the moment of commitment in your UX — don't POST until the user clicks Buy.

Mutations require the insurance:write scope; reads require insurance:read. Legacy keys without an explicit scope list are granted all scopes for backwards compatibility.

Lifecycle#

Enrollments move through up to six states. The transitions are deterministic and you can observe each one via webhook (see Webhooks).

StateMeaning
CREATEDBrief intermediate state immediately after POST /v1/enrollments. Transitions to PENDING_PAYMENT in the same request.
PENDING_PAYMENTIdentity verified; we're waiting for you to debit the user's wallet and call /payment-received.
PENDING_HMOPayment confirmed; we're calling the HMO to issue the policy. Usually milliseconds.
ACTIVEHMO returned a policy id. The user is covered. hmo_policy_id and hmo_member_id are now populated.
FAILEDTerminal. The HMO rejected the enrollment. Refund the user from the cancellation flow.
CANCELLEDTerminal. The user, fintech, or HMO terminated the policy. cancelled_at and cancellation_reason populated.

Enroll a member#

POST/v1/enrollments

Returns 202 Accepted with the enrollment record in PENDING_PAYMENT state. Identity verification runs synchronously before the response — see identity in the response.

Request body

FieldRequiredDescription
plan_idYesPierflow plan id from GET /v1/plans or a quote.
quote_idConditionalStrongly recommended. When supplied, the frozen wholesale + markup + splits from the quote are used. Without a quote we compute fresh splits at the wholesale price — which may differ from what the user saw.
fintech_user_refYesYour own user reference. Round-tripped on every event + webhook.
idempotency_keyNoAny opaque string. Same key under the same partner returns the existing enrollment for 24 hours — safe to retry.
identity.nin or identity.bvnOne required11-digit NIN or BVN. Stored as a SHA-256 hash + last-4; the plaintext is AES-256-GCM encrypted at rest and only ever decrypted by the identity service.
identity.full_nameYesMember's legal name.
identity.date_of_birthYesISO date (YYYY-MM-DD).
identity.sexNoM | F | U.
identity.phoneNoDisplay + future SMS notifications.
effective_fromNoISO datetime. When the policy should start. Defaults to the HMO's response, typically today.
bash
bash
curl -X POST https://sandbox.api.pierflow.com/v1/enrollments \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "plan_id": "plan_b3f9c21a",
    "quote_id": "quote_a1b2c3d4",
    "fintech_user_ref": "user_8821",
    "idempotency_key": "enr_user_8821_silver_v1",
    "identity": {
      "nin": "12345678901",
      "full_name": "Adaeze Margaret Nwosu",
      "date_of_birth": "1985-03-14",
      "sex": "F",
      "phone": "+2348012345678"
    }
  }'

Response

202 Accepted
json
{
  "enrollment": {
    "id": "enr_9k3jx2p7",
    "partner_id": "ptr_fintechco",
    "provider_id": "prov_reliance",
    "plan_id": "plan_b3f9c21a",
    "quote_id": "quote_a1b2c3d4",
    "fintech_user_ref": "user_8821",
    "hmo_policy_id": null,
    "hmo_member_id": null,
    "status": "PENDING_PAYMENT",
    "effective_from": null,
    "effective_to": null,
    "wholesale_ngn": "850000",
    "markup_ngn": "150000",
    "member_pays_ngn": "1000000",
    "splits_snapshot": { /* same shape as quote.splits_snapshot */ },
    "contract_version": 1,
    "created_at": "2026-06-08T12:00:00.000Z",
    "updated_at": "2026-06-08T12:00:00.000Z"
  },
  "identity": {
    "status": "AUTO_APPROVED",
    "confidence": 95,
    "provider": "STUB"
  },
  "idempotent_replay": false
}
Sandbox magic names. In sandbox, the identity stub looks at the full name: include TEST_FAIL and identity returns REJECTED (the enrollment is refused); include TEST_SOFT and you get SOFT_REVIEW (the enrollment proceeds but the identity row is flagged for manual review). Anything else returns AUTO_APPROVED at confidence 95.

Confirm payment received#

POST/v1/enrollments/:id/payment-received

Once you've debited the user's wallet for the full member_pays_ngn, call this endpoint. It moves the enrollment PENDING_PAYMENT → PENDING_HMO → ACTIVE by calling the HMO connector synchronously. Idempotent — calling twice after the enrollment is ACTIVE returns success without re-charging.

FieldRequiredDescription
amount_ngnYesAmount debited, in kobo. Must equal member_pays_ngn — we return AMOUNT_MISMATCH otherwise.
payment_refNoYour internal payment reference. Stored on the PAYMENT_RECEIVED event for reconciliation.
bash
bash
curl -X POST https://sandbox.api.pierflow.com/v1/enrollments/enr_9k3jx2p7/payment-received \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount_ngn": "1000000", "payment_ref": "wallet_txn_42" }'

Cancel an enrollment#

POST/v1/enrollments/:id/cancel

Terminates the policy. If the HMO already issued one we call the connector to terminate on their side too. Cannot be undone.

bash
bash
curl -X POST https://sandbox.api.pierflow.com/v1/enrollments/enr_9k3jx2p7/cancel \
  -H "Authorization: Bearer $PIERFLOW_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "user_requested" }'

Retrieve an enrollment#

GET/v1/enrollments/:id
GET/v1/enrollments?fintech_user_ref=...

Returns the same shape as the create response. The list-by-ref endpoint is useful for "show this user's policies" surfaces in the fintech app.

Settlement instruction#

Every enrollment carries splits_snapshot — the same shape you saw on the quote. Use it to credit each party in your ledger:

json
json
{
  "mode": "MARKUP_FIXED",
  "wholesale_ngn": "850000",
  "markup_ngn": "150000",
  "member_pays_ngn": "1000000",
  "hmo_line": { "role": "HMO", "amount_ngn": "850000", "settlement_tag": null },
  "lines": [
    { "role": "PIERFLOW", "amount_ngn": "20000", "settlement_tag": "pierflow:platform_fee", "is_remainder": false },
    { "role": "EMR_VENDOR", "amount_ngn": "30000", "settlement_tag": "emr_vendor:default", "is_remainder": false },
    { "role": "FINTECH", "amount_ngn": "100000", "settlement_tag": "fintech:self", "is_remainder": true }
  ]
}

The HMO is paid the wholesale amount directly. The platform parties (you, Pierflow, EMR vendor) split the markup. The settlement_tag on each line is your hint about where the credit goes in your ledger — agreed during HMO onboarding.

Lifecycle events & webhooks#

EventFires when
hmo_enrollment.createdPOST /v1/enrollments succeeded.
hmo_enrollment.identity_verifiedIdentity check returned AUTO_APPROVED or SOFT_REVIEW.
hmo_enrollment.identity_rejectedIdentity returned REJECTED. No enrollment row created. enrollment_id is null in the payload.
hmo_enrollment.payment_receivedPOST /payment-received succeeded; we're calling the HMO.
hmo_enrollment.submitted_to_hmoSame instant — included separately so you can correlate with HMO-side latency.
hmo_enrollment.activatedHMO confirmed the policy; status flipped to ACTIVE.
hmo_enrollment.hmo_rejectedHMO refused. Status flips to FAILED; this event is followed by hmo_enrollment.failed.
hmo_enrollment.failedTerminal failure event — pair with hmo_enrollment.hmo_rejected.
hmo_enrollment.cancelledPOST /cancel succeeded.

Idempotency#

Pass idempotency_key on POST /v1/enrollments. Repeated calls with the same key under the same partner within 24 hours return the existing enrollment unchanged — the response carries idempotent_replay: true so you can log it. The key is any string ≤200 chars; we recommend including your user reference and the plan id to disambiguate.

Quote binding#

When you pass quote_id, the enrollment inherits the quote's frozen wholesale + markup + splits exactly. This is the safe path: the numbers the user saw at comparison are the numbers they pay. If the quote has expired (24h after creation), the call returns 404 — re-quote and show the user the new numbers before charging.

Without quote_id, the enrollment is priced fresh from the current plan + active contract. The numbers may differ from what the user saw if the HMO updated the catalogue or you renegotiated the contract.