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.
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.
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.canSendA 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/createThe 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-TokenSubmission 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}/consentThe 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}The POA lands on the pre-filled consent document on your DocuSeal host and applies their signature. No AllCare login, no app install.
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-SHA256None of this is theoretical. Each card below points at the file doing the work right now.
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.tsxOne MediatR command runs DocuSeal create, PatientsV2 persist, then SMS, with archive-on-failure compensation.
faxintegrationservice /Application/Patient/Commands/CreateConsentSubmission/...Handler.csPosts submissions, retries 429 / 5xx on [1s, 2s, 4s], never logs the key, hashes the patient id for traces.
faxintegrationservice /Infrastructure/Services/DocuSealClient.csDedicated onboarding RingCentral app, separate from the fax pipeline's credentials so neither can break the other.
faxintegrationservice /Infrastructure/ConfigureService.RingCentralSms.csInline HMAC-SHA256 verification, idempotent de-dup, signed-PDF archival to blob, log-and-ack on non-terminal events.
faxintegrationservice /CommunicationHub.API/Controllers/DocuSealWebhookController.csConsent 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.csHost, 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.csThe 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"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 } } ] }
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.
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.
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.
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.
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.
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.
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.
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.
A witnessed counter-signature after the POA. This is a backend change, not just a builder change.
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.
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.