Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Build an iMessage Workflow in n8n

There is no dedicated Figment node in n8n. You connect to iMessage by calling Figment's REST API with n8n's standard HTTP Request node. The workflow pattern here adds two layers on top of that HTTP call: a human approval gate that holds every outgoing message until a reviewer confirms it, and an idempotency design that prevents duplicate sends when a step fails and is retried.

The workflow structure and node names are your design. The API endpoints and response shapes come from Figment.

What you need

  • A Figment connection with Messages ready. Check and start setup at /apple-connect/account. A signed-in Apple Account and a ready Messages service are distinct states; the API rejects send and read operations when the Messages service is not ready.
  • Both iMessage scopes granted to your app: messages:read (covers history and search) and messages:send (covers sends). Both scopes can be declared in one app registration and approved within a single grant, but they are issued separately. A conversation found in a read result is not automatically authorized for sends; the target conversation must be included in your messages:send scope when you register at /apple-connect/developers.
  • For SMS, MMS, and RCS: an eligible iPhone online, signed into the same Apple Account, with Messages in iCloud active. Apple's current guide (HT102545) confirms that Messages in iCloud covers SMS, MMS, and RCS alongside iMessage — a separate Text Message Forwarding toggle is redundant when Messages in iCloud is already on.
  • An n8n instance — self-hosted or n8n Cloud. The HTTP Request, Wait, Webhook, and Code nodes used here ship with every n8n edition.

Your connection_id (a con_... value) and a Figment access token are the two runtime credentials. Access tokens expire after 900 seconds — far shorter than a 24-hour approval wait. Because the approval gate suspends execution, the workflow must refresh the access token after approval and before the send request. The initial webhook payload must include the caller's current refreshToken. Your calling application must persist the rotated refresh_token returned by each refresh call; the previous value cannot be reused.

Store your credential in n8n

Create a Generic → Header Auth credential in n8n. Set the Name field to Authorization and the Value field to Bearer <your_access_token>. Use this credential for read operations, where your calling application supplies a current token at invocation time. The send step in the approval-gate workflow uses a dynamically refreshed token instead of this static credential.

The approval-gate workflow

StepNodePurpose
1Webhook TriggerReceives intent — chatGuid, text, actionKey, connectionId, refreshToken — from your application
2CodeValidates all required fields; throws on any missing value
3Notification (your design)Sends reviewer an approve/reject link embedding $execution.resumeUrl
4Wait — On Webhook CallPauses until the reviewer responds or the timeout fires
5IFRoutes on {"action":"approve"} in the resume callback body
6HTTP RequestRefreshes the Figment access token using the stored refreshToken
7HTTP RequestPOSTs send with the fresh token and Idempotency-Key header
8HTTP RequestGETs receipt using the same Idempotency-Key (requires messages:send)

Webhook Trigger. n8n generates a production URL when the workflow is activated and a test URL while editing. Your application POSTs the message intent here. The chatGuid value must come from a prior call to the chats endpoint — not from a manually constructed phone string.

Wait node. Set the resume option to On Webhook Call. The node exposes $execution.resumeUrl, an execution-scoped URL to embed in the Step 3 notification alongside the proposed message text and chatGuid. Set a reasonable timeout; 24 hours suits most team-review scenarios. If the timeout fires without a callback, the execution ends without sending.

IF node. Check that the resume body contains {"action":"approve"}. Route the false branch to a Stop and Error node so rejected attempts appear in n8n's execution history. Only the true branch continues to the refresh and send steps.

HTTP Request — refresh token (Step 6).

  • Method: POST
  • URL: https://figment.so/api/apple-connect/oauth/token
  • Body content type: application/x-www-form-urlencoded
  • Body fields: grant_type=refresh_token, client_id=<your_client_id>, refresh_token referenced from the Code node's output

The response includes a new access_token and a new refresh_token. Persist the new refresh_token in your application before proceeding; the previous value cannot be reused.

HTTP Request — send (Step 7). Set Authorization to Bearer <access_token> using an expression that references your Step 6 node's output by name — for example Bearer {{$('Refresh Token').first().json.access_token}}, where Refresh Token is your actual Step 6 node name. Reference connectionId, chatGuid, text, and actionKey from the Code node's output the same way. Do not use the static Header Auth credential here; it holds the token from before the approval wait.

  • Method: POST
  • URL: https://figment.so/api/apple-connect/v1/connections/{connectionId}/messages/send
  • Headers: Authorization: Bearer <refreshed_access_token>, Idempotency-Key: <actionKey>
  • Body (JSON): {"chatGuid":"<chatGuid>","text":"<text>"}

The response envelope is {data:{idempotencyKey,state,guid?,delivery?,retrySend?},mode,replayed}.

HTTP Request — receipt (Step 8). Poll after a submitted or unknown state response. Use the same fresh token as Step 7.

  • Method: GET
  • URL: https://figment.so/api/apple-connect/v1/connections/{connectionId}/messages/receipt
  • Headers: Authorization: Bearer <refreshed_access_token>, Idempotency-Key: <actionKey>

Receipt states: submitted, delivered, read, failed, unknown, simulated (sandbox only). submitted confirms the bridge accepted the request — not that iMessage delivered it to the recipient. An unknown state means no recorded outcome; poll the receipt and do not resubmit under a new key.

Idempotency keys: generate once, persist first

The bridge binds each idempotency key to one fixed combination of app, connection, and request body. A second request with the same key and identical body returns the already-recorded result; a changed body is rejected.

The key must be generated by your calling application and persisted to a durable store before the first send request reaches the network. Do not derive the key from a hash of the message body — two separate decisions to send identical text to the same person would share a key, suppressing the second send. Do not regenerate the key on retry; use the original persisted value.

This Code node validates that the upstream contract was met (design example — not executed):

// actionKey must be generated and persisted by your application
// before it POSTs to this workflow's webhook URL.
const { chatGuid, text, actionKey, connectionId, refreshToken } = $input.first().json;
if (!actionKey || !refreshToken) {
  throw new Error(
    'actionKey and refreshToken are required. Generate and persist the actionKey; ' +
    'supply the current refreshToken from your credential store.'
  );
}
if (!chatGuid || !text || !connectionId) {
  throw new Error('chatGuid, text, and connectionId are all required.');
}
return [{ json: { chatGuid, text, actionKey, connectionId, refreshToken } }];

If the send request at Step 7 times out before you receive a response, your persisted actionKey lets you poll the receipt endpoint safely — without risking a duplicate send under a new key.

Read conversations and messages

The same Header Auth credential works for all read operations. Use n8n's query-parameter fields rather than hand-crafting query strings: a bare + in a URL query decodes as a space, corrupting phone-number-shaped chatGuid values.

OperationMethod + path
List conversationsGET /v1/connections/{connection_id}/chats?limit=20&offset=0
Messages in a chatGET /v1/connections/{connection_id}/messages?chatGuid=<encoded>&limit=20
Search messagesGET /v1/connections/{connection_id}/messages/search?query=<encoded>&limit=20

List and search responses use the envelope {data:[...],mode,offset,nextOffset,hasMore,historyComplete,scope}. Paginate using hasMore and nextOffset; check historyComplete before assuming a full corpus — history on a recently connected Mac may be incomplete. The API returns a guid on each chat item; use that value as chatGuid in subsequent requests.

Pricing

Active live connections cost $29 per month per connection. Sandbox access is free and uses synthetic data; sandbox receipt states appear as simulated. Access starts after setup is complete.


Connect Apple Account · Manage connections and service status


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms