POA consent · new patient canvas · template 2

The signature flow is
already built.

Three things stand between it and working.

When an operator captures a POA on the canvas, AllCare can pre-fill the consent form, text or email the POA a signing link, and record the signature back on the patient. The code path exists end to end. What is missing is name-matching inside template 2 and one design decision: how many parties sign.

Built: frontend button + backend + DocuSeal + RingCentral + webhook Gap: role + field name match Decision: one signer vs two
The flow today

One click, seven hops.

This is what fires the moment an operator presses Send consent to POA on the patient-circle section of the canvas. Every hop below is real code in the repo today.

01
allcare-frontend · canvas
Operator presses "Send consent to POA"

The button only enables once the patient name, the POA name, and at least one valid channel (E.164 phone or a valid email) are present. The gate is the send-block itself.

DocuSealSendButton.canSend
02
frontend → CommunicationHub
POST the consent request

A small JSON payload goes to the fax-integration service: patient id, patient name, POA name, and whichever channel(s) the operator filled in.

POST /api/consent-submissions/create
03
CommunicationHub → DocuSeal (self-hosted)
Create the signing submission

The handler builds one submitter (the POA), pre-fills POA name and patient name, sets send_sms=false so AllCare owns SMS, and posts to your own DocuSeal instance with the API key.

POST {base}/api/submissions · X-Auth-Token
04
CommunicationHub → PatientsV2
Persist consent_status = sent

Submission id, submitter id, and the signing slug are written to the patient. If this write fails, the DocuSeal submission is archived so no orphaned signing thread stays live.

PATCH /api/Consent/{id}/consent
05
CommunicationHub → RingCentral
Text the POA a magic link

The link is built in memory only (never stored, never logged) and sent by SMS: "AllCare needs your signature on a consent form: {link}". SMS is non-fatal. If the POA has only an email, DocuSeal's own email handles delivery instead.

{base}/s/{slug}
06
POA · phone or email
POA opens the link and signs

The POA lands on the pre-filled consent document on your DocuSeal host and applies their signature. No AllCare login, no app install.

07
DocuSeal → CommunicationHub → PatientsV2
form.completed flips status to signed

DocuSeal calls back, the webhook verifies the HMAC signature, downloads the signed PDF to blob storage, and writes consent_status = signed with an append-only audit row. Duplicate callbacks are de-duped.

POST /api/DocuSealWebhook · HMAC-SHA256
What's already in the repo

Seven pieces, all present.

None of this is theoretical. Each card below points at the file doing the work right now.

Canvas send button + state machine

not_sent to sent to opened to signed / declined / expired, with a clipboard-only "Re-copy link" affordance.

allcare-frontend /libs/crm/.../new-patient-canvas/docuseal-send-button.tsx
Backend orchestrator

One MediatR command runs DocuSeal create, PatientsV2 persist, then SMS, with archive-on-failure compensation.

faxintegrationservice /Application/Patient/Commands/CreateConsentSubmission/...Handler.cs
DocuSeal HTTP client

Posts submissions, retries 429 / 5xx on [1s, 2s, 4s], never logs the key, hashes the patient id for traces.

faxintegrationservice /Infrastructure/Services/DocuSealClient.cs
RingCentral SMS outbound

Dedicated onboarding RingCentral app, separate from the fax pipeline's credentials so neither can break the other.

faxintegrationservice /Infrastructure/ConfigureService.RingCentralSms.cs
Signed webhook receiver

Inline HMAC-SHA256 verification, idempotent de-dup, signed-PDF archival to blob, log-and-ack on non-terminal events.

faxintegrationservice /CommunicationHub.API/Controllers/DocuSealWebhookController.cs
Consent state + audit on the patient

Consent status, submission ids, slug, signed-PDF blob uri, and an append-only consent audit log live on PatientsV2.

PatientsV2 /Domain/Entities/PatientConsentStatus.cs · ConsentAudit/ConsentAuditLog.cs
Key Vault config (opt-in)

Host, API key, template id, and webhook secret all resolve from Key Vault. If the key is not seeded, the flow simply stays dark and the rest of the service boots.

faxintegrationservice /Infrastructure/ConfigureService.DocuSeal.cs
Pre-fill: patient name + POA name

The handler already passes both names as submitter field values. Whether they actually land depends on the next section.

values = { "POA Name", "Patient Name" } · role = "First Party"
The exact request we send

This is the call. Two strings have to match template 2.

DocuSeal maps a submission onto a template by name. The submitter role and each values key are matched against the party names and field names defined inside template 2. A mismatch does not error. It silently skips.

POST https://docuseal.allcare.ai/api/submissions
X-Auth-Token: {allcare-docuseal-api-key-test}
{
  "template_id": 2,
  "send_email": true,            // only when the POA has an email
  "send_sms": false,            // AllCare owns SMS via RingCentral
  "external_id": "{patient-uuid}",
  "submitters": [
    {
      "role": "First Party",     // must equal a PARTY in template 2
      "name": "{POA name}",
      "phone": "{POA phone}",   // omitted if absent
      "email": "{POA email}",   // omitted if absent
      "external_id": "{patient-uuid}",
      "values": {
        "POA Name": "{POA name}",      // must equal a FIELD NAME in template 2
        "Patient Name": "{patient name}"  // must equal a FIELD NAME in template 2
      }
    }
  ]
}
What is missing

The mismatches.

Your screenshot of template 2 shows a party named "POA" and a field labelled "Legal Representative Name". The backend sends a role of "First Party" and a value keyed "POA Name". Here is what has to line up.

What the backend sends Must exist in template 2 as Your screenshot shows Verdict
role: "First Party" a signing party named exactly First Party party named "POA" MISMATCH
values["Patient Name"] a field named exactly Patient Name "Patient Name" LIKELY OK
values["POA Name"] a field named exactly POA Name "Legal Representative Name" MISMATCH
(nothing) "Relationship to Patient" present, left blank NOT PRE-FILLED

One caveat worth a 60-second check in the builder: DocuSeal matches on the field's internal name, not the visible title. A field can read "Legal Representative Name" on screen while its name is "POA Name". Double-click the field in the builder to confirm the actual name before assuming it is broken.

1. Role name must match the party

DocuSeal assigns each template field to a party (a "role"). The signature, date, and name fields you placed are owned by the party you called "POA". The backend invites a submitter with role "First Party". If those names differ, the POA is invited but the fields are not assigned to them, so they open a document with nothing to sign.

Fix: rename the template party to First Party, or change the one constant PoaDocuSealRole in the handler to "POA". Pick one name and make both sides agree.

2. Field names must match the value keys

The handler pre-fills two values: "POA Name" and "Patient Name". DocuSeal fills any field whose name matches a key and silently ignores the rest. "Patient Name" probably matches. "POA Name" against a "Legal Representative Name" field will not, so the POA's own name shows up blank on the form.

Fix: name the two fields exactly POA Name and Patient Name in the builder (the name, not the visible title), or change the value keys in the handler to match the field names.

3. Pre-filled values are editable, not locked

The backend uses DocuSeal's values map, which pre-fills a field but leaves it editable. On a consent document you almost certainly want the patient name and POA name locked so the signer cannot alter who the form is about.

Fix: mark each pre-filled field Read-only in the builder (the toggle on the field), per the "pre-fill template field values" guide. No code change needed.

4. consent_status is not on the patient GraphQL yet

The webhook writes consent_status = signed to PatientsV2, but the patient GraphQL query the canvas reads does not expose that field yet. So the button flips to "Sent" locally after a send, but it will not flip to "Signed" on its own when the POA finishes. The data is correct in the backend; the canvas just cannot see it.

Fix (follow-up): add consent_status to the patient GraphQL type and refetch after the webhook lands. Tracked as a deliberate follow-up, not a blocker for first signatures.

Your question: one signer, or two?

Keep it to one party.

The backend creates exactly one submitter today. That choice has a hard consequence: if template 2 has a second signing party with no matching submitter, the submission can never reach completed, so the form.completed webhook never fires and consent never flips to signed.

Recommended
Single party: the POA signs

Matches the backend as built. Patient name and POA name are read-only pre-filled text. The POA signature, date, and relationship belong to the one "First Party" role.

  • Works with the current handler, no backend change
  • The "Patient Signature" field gets removed or reassigned to the POA (the patient does not sign when a POA acts for them)
  • "Office Use Only" is internal text, not a signing party
  • form.completed fires the moment the POA signs
Only if compliance requires it
Two parties: AllCare counter-signs

A witnessed counter-signature after the POA. This is a backend change, not just a builder change.

  • Add a second submitter (the AllCare representative) in the handler
  • Set submission order to "preserved" so the POA signs first, then internal
  • Completion only fires after both parties finish
  • Adds a second async step and a second person to chase

For a patient consent the POA's signature is the legally operative act, so a single party is the right default. Do not add a second party in the builder expecting it to "just work": without a second submitter the document stalls forever.

The plan to finish it

Mostly builder work.

Every step is rooted in a DocuSeal guide, linked at the bottom. The code is done; the work is alignment, config, and one live test.

Phase 1
template 2

Align template 2 in the builder

  1. Name the signing party exactly "First Party" (or flip the handler constant to "POA").
  2. Name the two fields exactly POA Name and Patient Name (the field name, not the title).
  3. Mark both pre-filled fields Read-only so the POA cannot edit them.
  4. Assign the signature, date, and relationship fields to the single "First Party" role.
  5. Remove or reassign the separate "Patient Signature" field.
  6. docs: add-recipients-form-fields · pre-fill-template-field-values · embedded-text-field-tags
Phase 2
config

Seed Key Vault and the webhook

  1. Set allcare-docuseal-poa-template-id = 2.
  2. Confirm base url, API key, and webhook secret are all seeded in the test vault.
  3. Point the DocuSeal webhook at /fax-integration/api/DocuSealWebhook.
  4. Restart the service so the opt-in gate picks up the seeded key.
Phase 3
verify

One live signature, end to end

  1. Send to a test POA from the canvas.
  2. Confirm the link shows patient name and POA name, both locked.
  3. Confirm the SMS arrives (and the email, if both channels are set).
  4. Sign, then confirm consent_status flips to signed and the signed PDF lands in blob.
  5. docs: pre-fill-pdf-document-form-fields-with-api
Phase 4
optional

Follow-ups, once the basics sign

  1. Expose consent_status on the patient GraphQL so the canvas auto-shows "Signed".
  2. Pre-fill "Relationship to Patient" if you want it on the form.
  3. If a counter-signature is ever required, add a second submitter with order "preserved".
  4. docs: personalize-email-messages · pre-fill-pdf-document-form-fields-with-api
Config checklist

Five Key Vault values gate the whole flow.

  • allcare-docuseal-poa-template-id = 2The template you are editing. Numeric. Hard blocker if unset.
  • allcare-docuseal-base-url-testYour self-hosted host, e.g. https://docuseal.allcare.ai. Validated as a real URL at boot.
  • allcare-docuseal-api-key-testSent as X-Auth-Token. Its presence is the opt-in switch for the entire consent flow.
  • allcare-docuseal-webhook-secretHMAC key. If empty, the webhook fails closed with a 500 rather than trusting forged callbacks.
  • allcare-ringcentral-onboarding-*The onboarding RingCentral app. Separate from the fax pipeline's credentials on purpose.