Skip to content
fondline.← Back to FondlineLog in ↗

API REFERENCE

Quick startAuthentication & API keysGET · List campaignsPOST · Add contactsContact outcomesRepeat requestsErrorsSending behaviorRate limitsWebhooks & signaturesAPI scope
Get your API key
Building something?

We’re here to help

Contact support ↗
FONDLINE DEVELOPERS API v1

Good connections
Built into your workflow

Connect your own tools to Fondline Campaigns. Find a campaign, add contacts, and receive signed webhook events when conversations move forward.

BASE URLhttps://fondline.com/v1

Your first API call

Start with a read-only request to find the campaigns your key can access. You’ll need a Fondline account and an API key from Settings → Integrations.

  1. 01
    Create a key

    Choose its workspace access and save it securely. The key is shown only once.

  2. 02
    Find your campaign

    Run the request below and choose a campaign ID from the response.

  3. 03
    Add your contacts

    Send a batch of up to 500 contacts and check each contact’s result.

cURL
export FONDLINE_API_KEY="YOUR_API_KEY"

curl https://fondline.com/v1/campaigns \
  -H "Authorization: Bearer $FONDLINE_API_KEY"
Ready to enroll contacts?

Jump to the POST example. Adding contacts to an active campaign schedules outreach, so use a draft campaign while testing.

Getting a key

Make one in Settings → Integrations. It is shown once, at creation: Fondline stores a hash rather than the key, so a lost key is replaced rather than recovered.

HTTP header
Authorization: Bearer YOUR_API_KEY

When you make a key you choose what it reaches: one workspace, or every workspace you belong to. The second follows your membership rather than freezing it, so joining a team extends the key and leaving one withdraws it. A key can list campaigns and add contacts. It cannot start or pause a campaign, edit a sequence, read replies, or reach another workspace, so a leaked key costs you unwanted enrollments rather than your mail. Revoking takes effect on the next request.

Keep keys on a server. Anything shipped to a browser or a mobile app is readable by whoever has it. Use one key per system that calls us, named for that system, so revoking one never means guessing what else breaks.

Find a campaign

HTTP
GET /v1/campaigns

Up to 200 campaigns the key can reach, newest first, so you never have to copy an id out of a browser address bar. A key pinned to one workspace sees that workspace; a personal one sees every workspace you are in, and says which is which.

JSON response
{
  "campaigns": [
    {
      "id": "3a2d2882-0c4b-44b4-984a-1dc4f4a39c82",
      "name": "Q4 outreach",
      "status": "sending",
      "workspace": "NQB8 Team",
      "sends_from": "auren@example.com",
      "steps": 3,
      "contacts": 154
    }
  ]
}

status is the word the product uses beside the campaign: draft, sending, paused or completed. sends_from is the inbox its mail actually leaves from, which is worth checking before pointing a script at it.

Add contacts

HTTP
POST /v1/campaigns/{campaign_id}/contacts

Up to 500 contacts per request, as one transaction: it lands or it does not, so a failure halfway leaves nobody half-enrolled. Only email is required; first_name, last_name, company, title and linkedin fill the matching merge fields, and anything in custom becomes a merge field of its own

Send "already_contacted": true for somebody another tool has already been writing to. They are recorded against the campaign and never emailed by it, which is what a migration from another sequencer needs: those people have had two of the three messages already, and sending them again from a new address in a new thread is the worst first impression of a tool they did not know had changed. Dropping them instead would leave Fondline unaware they exist, and the next import would mail them.

cURL
curl https://fondline.com/v1/campaigns/3a2d2882-0c4b-44b4-984a-1dc4f4a39c82/contacts \
  -H "Authorization: Bearer $FONDLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "email": "ada@analytical.example",
        "first_name": "Ada",
        "company": "Analytical Engines",
        "custom": { "city": "London" }
      }
    ]
  }'
JSON response
{
  "campaign_id": "3a2d2882-0c4b-44b4-984a-1dc4f4a39c82",
  "enrolled": 1,
  "results": [
    {
      "email": "ada@analytical.example",
      "status": "enrolled",
      "contact_id": "c12acdaf-fa6f-472d-bbf6-a20e94af3d3a"
    }
  ]
}

enrolled counts only the people who will actually be emailed as things stand. A contact held for a missing field is not in that number, because counting it would promise a send that is not going to happen.

What each contact comes back as

The response is 200 whenever the request was understood, even if nobody was enrolled: a partial success is the normal case, and a 4xx would misdescribe the people who did go in.

StatusMeansWhat to do
enrolledIn the campaign and due to be emailedNothing; sending follows the campaign schedule
already_enrolledThey were already in this campaignNothing; any new details were merged in
needs_fieldsIn the campaign, held back: the sequence uses a merge field this contact has no value forSend the missing fields later, or fill them in Fondline; sending resumes by itself
suppressedThis address hard-bounced before, or asked not to be contacted, so it was not enrolledStop sending it; this one is never overridable
invalid_emailNot a usable addressFix it upstream; it was not stored
recorded_not_mailedIn the campaign and stopped, because you sent already_contacted for themNothing; they are on record and will never be emailed by this campaign
duplicate_in_requestThe same address appeared earlier in this requestNothing; the first one was used

Sending the same list twice

Safe, and no idempotency key is needed. A contact is identified by campaign and address, so a repeat returns already_enrolled does not repeat the enrollment: no second enrollment, no second email, no reset of where they are in the sequence. New details on a repeat are merged in, which is how you fill a gap the response told you about.

Errors

These describe the request itself. Anything wrong with an individual contact comes back as an outcome above, not as a status code.

401No key, or the key is not recognised
404No campaign with that id in this workspace
413More than 500 contacts; split the batch
422The body was not understood, or the campaign has no sequence yet
429Rate limited; Retry-After says how long

What it promises

  • Suppression is never bypassed. An address that hard-bounced or opted out stays refused, however it arrives. There is no flag to override it.
  • Nothing sends immediately. Enrolling schedules. The send window, work-day rule, daily cap and jitter all still apply, so a bulk import does not become a bulk send.
  • Nobody is emailed with a blank. A contact missing a merge field the sequence uses is admitted and held, not quietly sent a message with a hole in it.
  • Replies still stop the sequence. Contacts added this way are ordinary contacts.

Limits

  • 500 contacts per request
  • 120 requests a minute, per key
  • 1 MB request body

Rate-limited and accepted authenticated requests carry X-RateLimit-Remaining and X-RateLimit-Reset, so you can slow down before being refused rather than after. Past the limit you get 429, and Retry-After says how many seconds to wait

Webhooks

Add an endpoint in Settings → Integrations and Fondline posts to it, so you do not have to ask. Every delivery is signed, retried while your server is unavailable, and switched off after enough consecutive failures.

messagedA step went out to a contact
repliedSomebody answered, and their sequence stopped
bounced_hardThe address is dead and was suppressed
bounced_softA temporary failure
auto_replyAn out-of-office, which does not stop the sequence
HTTP
POST https://your-server.example/fondline
Fondline-Signature: t=1788547200,v1=6f2c…
Fondline-Delivery: 8f14e45f-ceea-467a-9b1e-2a4c9d3f1b70

{
  "id": "8f14e45f-ceea-467a-9b1e-2a4c9d3f1b70",
  "type": "messaged",
  "created_at": "2026-09-05T15:12:04Z",
  "campaign": { "id": "3a2d2882-…", "name": "Q4 outreach" },
  "contact": {
    "id": "c12acdaf-…",
    "email": "ada@analytical.example",
    "first_name": "Ada",
    "last_name": "Lovelace"
  },
  "subject": "Quick question, Ada",
  "step": 1
}

step is present on messaged and counts from 1. On the others it is absent, because the step is not the thing that happened. There is never a message body: Fondline reads headers only, so the subject is all there is to forward.

Checking the signature

A URL is a secret nobody keeps, so verify before you act. The timestamp is signed with the body, which is what stops a captured delivery being replayed at you later; reject anything older than about five minutes.

Node.js · Signature verification
import crypto from "node:crypto";

function verifyWebhook(secret, rawBody, header) {
  if (typeof header !== "string") return false;
  const parts = Object.fromEntries(
    header.split(",").map(part => part.trim().split("="))
  );
  const { t, v1 } = parts;
  if (!/^\d+$/.test(t ?? "") || !/^[0-9a-f]{64}$/.test(v1 ?? "")) {
    return false;
  }
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto.createHmac("sha256", secret)
    .update(t + "." + rawBody).digest();
  const received = Buffer.from(v1, "hex");
  return received.length === expected.length &&
    crypto.timingSafeEqual(expected, received);
}

Answer 2xx quickly and do your work afterwards. Anything else is a failure and will be retried, backing off from about a minute to an hour, and an endpoint that fails long enough is switched off with the reason recorded where you added it.

Fondline-Delivery is the same on every retry of one event, so it is what to key on if you need to tell a retry from something new.

Not in v1

Starting or pausing a campaign, removing contacts, and creating campaigns. Deciding that mail goes out stays a decision somebody makes in the product, where the consequences are on screen.

Questions: support@fondline.com

© 2026 Fondline LLC
API docsPrivacy policyTerms of serviceContact