Send a handwritten card from a Salesforce Flow
When an opportunity closes, mail the contact a card written with a real pen. A record-triggered flow calls a small Apex action, and a Named Credential holds your notecardHQ key. There is no notecardHQ app to install.
Why Apex, not the HTTP Callout action
Flow's HTTP Callout action calls an API without code, using a named credential for authentication (source). Salesforce notes that request headers are not supported on the action itself: headers belong in the credential (source). That covers Authorization, but notecardHQ also needs an Idempotency-Key that is different for every record, so a retry never mails twice, and a credential header is the same for every call. A small Apex action sets that header per record and keeps the key in the credential. (Salesforce also allows editing the generated OpenAPI specification of the external service to add headers (source); the Apex route below is simpler to review.)
What you need
- An edition with Apex: Salesforce lists Enterprise, Performance, Unlimited and Developer Editions, and Database.com (source).
- The Customize Applications permission to create credentials (source), and a sandbox to deploy Apex from.
- A notecardHQ API key, test key first.
1. External credential: the Bearer key
In Setup, Named Credentials, External Credentials tab (source):
- Create NotecardHQ with the Custom authentication protocol, which Salesforce uses for a key or token (source).
- Add a permission set mapping and, in it, an authentication parameter named
ApiKeywhose value is your key. - Add a custom header named
Authorizationwith the value{!'Bearer ' & $Credential.NotecardHQ.ApiKey}. - Use a permission set that grants access to the external credential and to the User Credentials object, and assign it to the users whose changes start the flow (source).
2. Named credential: the workspace URL
Create a named credential called NotecardHQ with the URL https://you.notecardhq.com and the external credential above. Clear Generate Authorization Header, which Salesforce says to clear for key or token APIs, and select Allow Formulas in HTTP Header, since the header value is a formula (source). Apex then calls callout:NotecardHQ/api/v1/notes, and Salesforce adds the authentication (source).
3. The Apex action
The @InvocableMethod annotation makes the method an action in Flow Builder (source). A callout cannot run while the same transaction has pending work, such as the record save that started the flow (source), so the action hands the notes to a queueable job, which may make callouts when it implements Database.AllowsCallouts (source).
public with sharing class NotecardHqSend {
public class Request {
@InvocableVariable(required=true) public Id recordId;
@InvocableVariable(required=true) public String firstName;
@InvocableVariable public String lastName;
@InvocableVariable public String company;
@InvocableVariable(required=true) public String street;
@InvocableVariable(required=true) public String city;
@InvocableVariable public String state;
@InvocableVariable public String postalCode;
@InvocableVariable(required=true) public String countryCode;
@InvocableVariable(required=true) public String message;
}
@InvocableMethod(label='Send handwritten card (notecardHQ)')
public static void send(List<Request> requests) {
System.enqueueJob(new Sender(requests));
}
public class Sender implements Queueable, Database.AllowsCallouts {
private List<Request> requests;
public Sender(List<Request> requests) { this.requests = requests; }
public void execute(QueueableContext ctx) {
for (Request r : requests) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:NotecardHQ/api/v1/notes');
req.setMethod('POST');
req.setTimeout(30000);
req.setHeader('Content-Type', 'application/json');
// One card per opportunity, however often the flow fires.
req.setHeader('Idempotency-Key', 'salesforce:closed-won:' + r.recordId);
req.setBody(JSON.serialize(new Map<String, Object>{
'externalId' => String.valueOf(r.recordId),
'recipient' => new Map<String, Object>{
'firstName' => r.firstName,
'lastName' => r.lastName == null ? '' : r.lastName,
'businessName' => r.company == null ? '' : r.company,
'address1' => r.street,
'city' => r.city,
'state' => r.state == null ? '' : r.state,
'postalCode' => r.postalCode == null ? '' : r.postalCode,
'country' => r.countryCode
},
'content' => r.message
}));
HttpResponse res = new Http().send(req);
if (res.getStatusCode() >= 300) {
System.debug(LoggingLevel.ERROR, 'notecardHQ ' + res.getStatusCode() + ' ' + res.getBody());
}
}
}
}
}
Deploy it with a test class that mocks the callout, the way your org ships any Apex.
4. The flow
- Create a record-triggered flow, for example on Opportunity when the stage becomes Closed Won.
- Add an Action element and pick Send handwritten card (notecardHQ).
- Map
recordIdto the opportunity id, the name and mailing address fields from the opportunity's contact,countryCodeto a two-letter country code, andmessageto a text template that greets the contact by first name and signs off from your team.
The request it sends
POST https://you.notecardhq.com/api/v1/notes
Authorization: Bearer mos_test_...
Content-Type: application/json
Idempotency-Key: salesforce:closed-won:006Ho00000AbCdEIAV
{
"externalId": "006Ho00000AbCdEIAV",
"recipient": {
"firstName": "Sam", "lastName": "Lee", "businessName": "Acme",
"address1": "123 Main St", "city": "Austin",
"state": "TX", "postalCode": "78701", "country": "US"
},
"content": "Hi Sam,\n\nThank you for choosing us.\n\nThe team"
}
A stable Idempotency-Key
The key is the record id plus the event: salesforce:closed-won:006Ho00000AbCdEIAV. If the opportunity is reopened and closed again, or the job runs twice, notecardHQ returns the original note instead of writing a second card. To thank a contact once across all their deals, key it on the contact id instead.
Build with a test key
Create a test key (mos_test_) under Integrations in the portal, put it in the ApiKey parameter, and use it until the whole flow works. A test key runs the same validation as a live key, but the note is never written, mailed or billed. Then switch to your live key and send one card to your own address before you turn the automation on for customers.
Limits and gotchas
- One Apex transaction can make at most 100 callouts (source), and each job above sends one request per record. If a data load can close more than 100 opportunities at once, split the list into several jobs.
- The default callout timeout is 10 seconds; the code sets 30, within Salesforce's 120-second maximum (source).
- Errors are written to the debug log, not retried. Re-running the action for the same record is safe: the same key never sends twice.
- Users who can trigger the flow need the permission set with access to the external credential (source).
- The country must be a two-letter code. If your org stores the country as text, convert it in a formula before the action.
- The message is written exactly as sent, up to 500 characters including the greeting and sign-off.
Errors
- 401. Missing or wrong key. Fix the key; do not retry.
- 409. The Idempotency-Key was already used with a different body. Stop and check; never change the key to get past it.
- 422. A missing field, a character the pen cannot write in the note (for example an emoji; accented letters are written without the accent), or an undeliverable US address. Nothing was created; fix the data.
- 429 or 5xx. Temporary. Retry with the same key and the same body.
Which moments to use, from closed won to renewals scheduled weeks ahead: handwritten notes from Salesforce.
Vendor details checked on October 26, 2026 against Salesforce HTTP Callout, HTTP Callout considerations, authenticating HTTP Callout actions, named credentials in Apex, InvocableMethod, Queueable Apex, callout limits, Apex editions. Platforms change their plans and features; check the source before you build.
Field rules, statuses and errors are in the API reference. Prefer another language? See the quickstart.
Send your first note.
Tell us what you want to send and we will set up your workspace.