Salesforce integration guide
A suggested build flow. Follow it in order and you get a working, demoable app: verify providers from Salesforce and watch Curacheck decisions land inside Salesforce.
What you're building
Both directions of the integration, in one Lightning app. One provider you verify from the Screen Flow round-trips back in as an inbound event record — a single action exercises both directions.
Outbound (verify from Salesforce)
┌── Salesforce ──────────────┌ ┌── Curacheck ────────────┌
│ Screen Flow "Verify │ POST │ POST /v1/batches │
│ Provider" ── Apex action ──┼────────│ runs verification │
│ │ │ │
│ Curacheck Events tab ◄───┼────────────┴ webhook (HMAC-signed) │
│ Dashboard + Report │ Inbound │ verification.completed │
└────────────────────────────┘ └────────────────────────┘
Two receiver patterns — pick one
| Pattern | Inbound events do what | Use when |
|---|---|---|
| A — correlate to your records | Match externalId (the SF record Id you submitted) → update your Provider__c / Contact | You always submit rosters from Salesforce and want the decision written back onto that record. |
| B — event log + dashboard (this guide) | Upsert one Curacheck_Event__c per event; surface as a tab + dashboard | You want every verification visible regardless of where it originated (bulk, portal, API), plus a demoable dashboard. |
Pattern A is the default of the full admin runbook. Pattern B is what this guide builds — it works even for events with no externalId (e.g. bulk verifications), which Pattern A silently drops.
Part 1 · Inbound: see verifications in Salesforce
1 · Get the credentials from Curacheck
In the Curacheck app: Integrations → Salesforce.
- Generate a Salesforce key — one bundled key (
batches:write,batches:read,review:write). Copy it from the inline reveal; shown once. - Generate signing secret — copy it from the inline reveal; shown once. Paste your endpoint URL (from step 4) into the Events to Salesforce box and subscribe
verification.completed.
2 · Create the event object
Custom object Curacheck_Event__c (enable Allow Reports — you need it for the dashboard):
| Field | Type | Notes |
|---|---|---|
Event_Id__c | Text(64), External ID, Unique | payload id; the upsert key |
Event_Type__c | Text(64) | verification.completed |
Decision__c | Text(32) | auto_approve / review / reject |
NPI__c | Text(20) | provider.npi |
Name (Text) holds the provider name.
3 · Deploy the Apex receiver
An @RestResource class that verifies the HMAC signature, dedupes on the event id, and — in Pattern B — upserts a Curacheck_Event__c keyed on Event_Id__c:
Curacheck_Event__c rec = new Curacheck_Event__c(
Name = providerName, Event_Id__c = eventId,
Event_Type__c = eventType, Decision__c = decision, NPI__c = npi);
upsert rec Event_Id__c; // dedupe by external id
The signature header is X-Attesta-Signature: t=<unix>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>.<raw body>") — verify it against the raw request body.
Read the header case-insensitively. Salesforce Sites serve HTTP/2, which lowercases header names, so req.headers.get('X-Attesta-Signature') returns null and every event 401s. Iterate req.headers.keySet() instead. (For the full class, contact support@curacheck.io for the admin runbook.)
4 · Expose it via a Site — and grant the guest user FLS
A publicly reachable @RestResource needs a Salesforce Site (Setup → Sites). The webhook URL is https://<domain>.my.salesforce-sites.com/<path>/services/apexrest/<urlMapping>. Paste it into the Curacheck Events to Salesforce box (step 1).
On the Site's guest user profile grant all three:
- Apex Class Access → the receiver class.
- Object CRUD → Read + Create on
Curacheck_Event__c. - Field-Level Security → Read + Edit on every custom field (
Event_Id__c,Event_Type__c,Decision__c,NPI__c).
⚠ The #1 thing that silently breaks. Guest-user security enforces field permissions at the DML layer. Miss the FLS and the receiver's upsert throws, the partial-success write swallows it, and the endpoint still returns 200 — so Curacheck marks the webhook delivered while zero rows save. If deliveries read 200 but the object stays empty, the guest profile is missing FLS.
Grant it on the profile, or set it in bulk with anonymous Apex by inserting FieldPermissions on the profile's owned permission set:
PermissionSet ps = [SELECT Id FROM PermissionSet
WHERE IsOwnedByProfile = true AND ProfileId = :guestProfileId LIMIT 1];
List<FieldPermissions> fps = new List<FieldPermissions>();
for (String f : new List<String>{'Event_Id__c','Event_Type__c','Decision__c','NPI__c'}) {
fps.add(new FieldPermissions(ParentId = ps.Id, SobjectType = 'Curacheck_Event__c',
Field = 'Curacheck_Event__c.' + f, PermissionsRead = true, PermissionsEdit = true));
}
Database.insert(fps, false);
5 · Build the tab, report, and dashboard
- Tab — Setup → Tabs → New Custom Object Tab →
Curacheck Event. Pick any style. (A custom object needs a tab before it can join an app's navigation.) - Report — a summary report grouped by
Decision__c. - Dashboard — a donut on that report + a record-count KPI + a detail table. Put both in a shared folder.
Inbound is now working: verifications show up as records and roll up on the dashboard.
Part 2 · Outbound: verify a provider from Salesforce
1 · Store the API key (Named Credential + External Credential)
Named Credential Curacheck → https://api.curacheck.io; External Credential Curacheck API (Custom auth) holds the key; custom header:
x-api-key = {!$Credential.Curacheck_API.ApiKey}
Two-part path only. Adding the principal (…Curacheck_API.CuracheckPrincipal.ApiKey) throws System.CalloutException: Field … does not exist at callout time. Assign the integration permission set (with External Credential Principal Access) to whoever runs the flow.
2 · Apex invocable — submit + poll for the decision
Curacheck verifies asynchronously: POST /v1/batches returns a batchId immediately; the decision lands a moment later. This invocable posts the batch, polls GET /v1/batches/{id}/rows until the row resolves, and returns the decision so a Screen Flow shows it inline. (Needs batches:read — the bundled key has it.)
public with sharing class CuracheckVerifyAction {
public class Request {
@InvocableVariable(label='First Name' required=true) public String firstName;
@InvocableVariable(label='Last Name' required=true) public String lastName;
@InvocableVariable(label='NPI' required=true) public String npi;
@InvocableVariable(label='State') public String state;
}
public class Result {
@InvocableVariable public String decision; // auto_approve | review | reject | pending
@InvocableVariable public String statusMessage; // human summary for the result screen
@InvocableVariable public String batchId;
}
@InvocableMethod(label='Verify Provider with Curacheck')
public static List<Result> verify(List<Request> reqs) {
List<Result> out = new List<Result>();
for (Request r : reqs) out.add(runOne(r));
return out;
}
private static Result runOne(Request req) {
Result r = new Result();
Map<String,Object> prov = new Map<String,Object>{
'npi'=>req.npi, 'firstName'=>req.firstName, 'lastName'=>req.lastName };
if (String.isNotBlank(req.state)) prov.put('state', req.state);
HttpRequest post = new HttpRequest();
post.setEndpoint('callout:Curacheck/v1/batches');
post.setMethod('POST');
post.setHeader('Content-Type','application/json');
post.setBody(JSON.serialize(new Map<String,Object>{
'name'=>'Salesforce Flow verification', 'providers'=>new List<Object>{ prov } }));
HttpResponse pres = new Http().send(post);
Map<String,Object> pbody = (Map<String,Object>) JSON.deserializeUntyped(pres.getBody());
r.batchId = (String) pbody.get('batchId');
String decision = 'pending';
for (Integer i = 0; i < 10 && decision == 'pending'; i++) {
HttpRequest get = new HttpRequest();
get.setEndpoint('callout:Curacheck/v1/batches/' + r.batchId + '/rows');
get.setMethod('GET');
HttpResponse gres = new Http().send(get);
if (gres.getStatusCode() == 200) {
Map<String,Object> g = (Map<String,Object>) JSON.deserializeUntyped(gres.getBody());
List<Object> rows = (List<Object>) g.get('rows');
if (rows != null && !rows.isEmpty()) {
Map<String,Object> row0 = (Map<String,Object>) rows[0];
String st = (String) row0.get('status');
if (st != null && st != 'pending') decision = (String) row0.get('decision');
}
}
}
r.decision = decision;
r.statusMessage = decision == 'pending'
? 'Submitted (batch ' + r.batchId + '). Result will appear in Curacheck Events shortly.'
: 'Decision: ' + decision;
return r;
}
}
Latency note. The poll blocks the running user until the decision resolves (seconds, occasionally ~40s for a cold provider). Apex has no sleep, so the loop paces on callout round-trips. If a snappier UX matters, drop the poll, return "submitted", and let the decision surface in the Events tab (Part 1) via the webhook instead of inline.
3 · Screen Flow
Setup → Flows → New Flow → Screen Flow:
- Screen "Verify a Provider" — Text inputs: Last Name, First Name, NPI (required), State (optional).
- Action — the
Verify Provider with Curacheckinvocable. Map each input to the matching screen field ({!Screen.FieldApiName}). Its outputs auto-store. - Screen "Result" — a Display Text with
{!Action.statusMessage}.
Activate it.
4 · Surface it in the app
App Manager → your app → Utility Items → Add Utility Item → Flow → pick the Screen Flow, label it "Verify Provider". It becomes a persistent launcher in the app's bottom bar.
Bundle it into one app
App Manager → New Lightning App "Curacheck". Navigation items: Dashboards, Reports, Curacheck Events. Add the Verify Provider utility item. Assign the profiles who need it. Now everything — verify a provider, watch the decision arrive, browse the event log, see the dashboard — lives under one App Launcher tile.
Gotchas checklist
- Guest FLS on
Curacheck_Event__cfields (Part 1 · 4). The silent-200 trap. - HTTP/2 header case — read
X-Attesta-Signaturecase-insensitively. - Two-part credential formula —
{!$Credential.Curacheck_API.ApiKey}, no principal. batches:readscope on the key if the outbound action polls for the decision.- A custom object needs a tab before it can join an app's navigation.
References
- Rendered API reference: api.curacheck.io/docs
- OpenAPI spec (import into Salesforce External Services / Postman): openapi.json
- General integration hub: Developers
- Full admin runbook (every field, the complete Apex receiver, event schema, limits): request from support
Questions? support@curacheck.io.