Build an integration
The Zapier, Make and n8n apps and the WordPress plugin that ReputeCast publishes use only the public REST API. This page shows how to build email marketing integrations the same way: authenticate, find the workspace, subscribe to events and call the five actions the official apps offer, without ever undoing a contact’s consent.
Before you begin
Section titled “Before you begin”- A ReputeCast instance reachable over HTTPS from where your integration runs, and its public URL (the “instance
URL”). Every path below starts with
/api/v1. - An API key. For your own app or service, create one in Settings, then Keys & MCP with
Create API key and give it only what the integration needs:
webhooks:managefor triggers,subscribers:createandsubscribers:editfor contact actions,transactional:sendto send transactional email. See Authentication and API keys. - The official apps get a different kind of key, an integration key, from Settings, then Integrations › Connect. ReputeCast creates integration keys only for Zapier, Make, n8n and WordPress (Connect other tools).
Connect and test
Section titled “Connect and test”-
Ask the user for the instance URL and the key. Store the key encrypted; send it as
Authorization: Bearer rc_live_…on every call. -
Call
GET /api/v1/integration. Any API key may call it, with no permission, and it answers which workspace the key belongs to:Terminal curl https://reputecast.example.com/api/v1/integration \--header "Authorization: Bearer $REPUTECAST_API_KEY"200 OK {"workspace": "acme-newsletter","workspaceName": "Acme Newsletter","integration": "","key": "CRM sync","triggers": [],"actions": [],"permissions": ["subscribers:create", "subscribers:edit", "webhooks:manage"]} -
Use
workspacein every other path (/api/v1/workspaces/acme-newsletter/…) and checkpermissionsbefore offering a feature.integration,triggersandactionsare filled only for integration keys.
Triggers: subscribe to events
Section titled “Triggers: subscribe to events”Your integration hears about changes through outgoing webhooks. When a user turns on a trigger, create an endpoint for it; when they turn it off, delete it.
curl --request POST \ --url https://reputecast.example.com/api/v1/workspaces/acme-newsletter/webhooks \ --header "Authorization: Bearer $REPUTECAST_API_KEY" \ --header 'Content-Type: application/json' \ --data '{ "name": "CRM sync · unsubscribes", "url": "https://crm.example.com/hooks/reputecast", "events": ["contact.unsubscribed"] }'The 201 answer carries the endpoint under webhook and its signing secret under secret, shown only in this
answer. Store the secret with the subscription and verify every delivery with it
(verify the signature). DELETE …/webhooks/<webhookId> unsubscribes.
The events most integrations offer as triggers:
| Trigger | Event | The key needs |
|---|---|---|
| Contact subscribed | contact.subscribed |
subscribers:view |
| Contact unsubscribed | contact.unsubscribed |
subscribers:view |
| Contact updated | contact.updated |
subscribers:view |
| Form submitted | form.submitted |
subscribers:view |
| Campaign sent | campaign.completed |
campaigns:view |
| Transactional message bounced | message.bounced |
transactional:view |
Every other event in Webhook events works the same way. form.submitted fires when a
signup is confirmed (at once for single opt-in) and carries the form’s ID, so your app can filter by form.
The official apps use REST hooks instead: POST …/integration/hooks with { "event", "url" } creates a managed
endpoint, and an endpoint that answers 410 Gone is removed. Those operations, and the sample, list, field and
template lookups under …/integration/, accept only integration keys
(Integration API reference).
Actions
Section titled “Actions”| Action | Call | The key needs |
|---|---|---|
| Create or update a contact | POST …/contacts/upsert |
subscribers:create; subscribers:edit to update a contact |
| Add a contact to a list | POST …/lists/<listId>/contacts |
subscribers:edit |
| Add tags to a contact | POST …/contacts/tags |
subscribers:edit |
| Record an event | POST …/automations/events |
subscribers:edit |
| Send a transactional email | POST …/transactional/messages |
transactional:send (sensitive) |
Create or update a contact matches by email, so repeating it updates the same contact instead of creating a second one:
# REPUTECAST_API_KEY: an API key (rc_live_…) with subscribers:create (and subscribers:edit to update contacts).# Replace reputecast.example.com with your public URL and your-workspace with your workspace slug.# List IDs come from GET /api/v1/workspaces/your-workspace/lists.curl --request POST \ --url https://reputecast.example.com/api/v1/workspaces/your-workspace/contacts/upsert \ --header "Authorization: Bearer $REPUTECAST_API_KEY" \ --header 'Content-Type: application/json' \ --data '{ "email": "[email protected]", "firstName": "Sam", "lastName": "Rivera", "language": "en-US", "tags": ["trial"], "lists": ["0192f1c4-3a5b-7c2d-8e9f-1a2b3c4d5e6f"], "consent": "double" }'consent: "single"adds the contact to the lists now;"double"sends the confirmation email first, and the list showspendinguntil the click. Withoutconsent, each list’s own setting applies.languagetakes a BCP 47 code. Values such asSpanishores_MXare normalized (es,es-MX); an unknown value answers422, naming the closest match when there is one.- Lists hold up to 20 IDs per call and tags up to 50. Get list IDs from
GET …/listsand custom field keys forfieldsfromGET …/fields(both needsubscribers:view). - The answer says whether the contact was
created, which lists it joined with which status, and whether a confirmation email went out (confirmationSent).
Add a contact to a list needs an existing contact; a new address answers 404, so call the upsert first.
Record an event is covered in Record events and conversions, and
Send a transactional email in Send transactional email with the API.
Consent is kept
Section titled “Consent is kept”An action never adds someone back to a list they left and never adds a suppressed address. Instead, it answers
409:
{ "status": 409, "title": "Conflict", "code": "unsubscribed", "detail": "[email protected] left Beta Program on 2 Oct. ReputeCast doesn't add people back to a list they left.", "meta": { "list": "Beta Program", "listId": "0192…", "leftAt": "2026-10-02T13:41:00Z" }}409 suppressed carries meta.reason and meta.since. Treat both as “skipped” for that record, not as a failed
workflow: report it to the user and carry on with the rest.
Retries and idempotency
Section titled “Retries and idempotency”- The transactional send and the conversions API accept an
Idempotency-Keyheader: a retry with the same key gets the first answer and doesn’t send or record twice. Use your platform’s run or record ID in the key, as the official apps do (make-<scenario ID>-<value>,n8n-<workflow ID>-<value>). - The other actions don’t take a key. Retry them only after a network error or a
429/5xxanswer, and never after a2xx. - Answer
429by waiting forRetry-After. The general limit is 600 requests per minute per key (Rate limits).
Integration keys and workflow names
Section titled “Integration keys and workflow names”If you maintain one of the official apps, its key comes from Settings, then Integrations and follows extra rules:
-
It works only for the operations of the triggers and actions the user turned on, even where its permissions would allow more. Anything else answers
403 integration_operation_denied. -
Rotate key keeps the old key working for 24 hours, so the app can be updated without failed runs; Stop the old key now ends the overlap.
-
When the key’s owner is deactivated or removed, calls answer
403 owner_deactivateduntil a member withintegrations:manageselects Take over. A revoked or expired key answers401 key_revokedor401 key_expired. -
Every call is kept for 7 days in the integration’s call log. Send the workflow’s name and ID with each call so the log can show them:
Request header X-ReputeCast-Workflow: Beta application (Typeform) → ReputeCast contact; id=284113907Percent-encode (UTF-8) characters outside printable ASCII and
%,;,=and"; ReputeCast decodes them.name="…"and RFC 8187name*=UTF-8''…forms are accepted too.
| Setting | Default | How to change |
|---|---|---|
| Old key keeps working after Rotate key | 24 hours | Stop the old key now ends it early |
| Call log kept | 7 days | Fixed |
| Needs attention when the key expires within | 7 days | Fixed |
| Needs attention when a managed webhook keeps failing for | 30 minutes | Fixed |
| Needs attention when calls fail | More than 50% of calls in the last hour (at least 4 calls) | Fixed |
| Needs attention when a WordPress site stops reporting for | 24 hours | Fixed |
| WordPress plugin reports to ReputeCast | On connect, every 12 hours and with every post | Fixed |
| Check now timeout for a WordPress site | 10 s | Fixed |
Check that it worked
Section titled “Check that it worked”GET /api/v1/integrationanswers200with the workspace and permissions you expect.- After creating an endpoint, Settings, then Webhooks lists it, and its Send a test event reaches your receiver.
- After an upsert, the contact appears in Audience with the lists, tags and consent you sent.
Troubleshooting
Section titled “Troubleshooting”403 permission_denied: the key lacks the permission named inpermission; thehelpersfield names members who can grant it.422 private_addresswhen creating an endpoint: the URL points to a private network. Use a public address, or ask an instance admin to setwebhooks.allow_private_networks.404on add to list: the contact doesn’t exist yet; upsert it first.409 unsubscribedor409 suppressed: expected; skip the record.403 integration_operation_denied: the integration key isn’t allowed this call. Change what the integration can do in Settings, then Integrations.