Building Your First Automation with APIs

Build a multi-step automation that connects two platforms via API

Welcome Back!

In Chapter 1, you installed Claude Code and built your first quick win. You proved you can use the terminal.

Now we're building something real: automations that connect multiple tools and save us hours of manual work every week.

I recorded three videos of me building this automation from scratch. You'll watch what I do, then read the technical explanations of what's actually happening under the hood. The videos show the "how." The written sections explain the "what" and "why."

By the end, you'll understand:

  • What an API actually is (and why it matters)

  • The difference between reading data, creating data, and updating data

  • How to connect any two tools that have APIs

  • What to do when things break

Let's go!

Video 1: What We're Building (and Why)

In this video, I explain the problem. I run an invite-only newsletter called Field Notes, and I want to automate the process of creating member accounts. I walk through the tools involved (Tally for forms, Airtable for tracking, Ghost for the newsletter) and what we're going to automate.

The key takeaway: I want to do two things:

  1. Check who's already a member from the first batch and update Airtable to reflect that

  2. Set up a way to approve new members and create their accounts in batch — no more doing it one by one

First, Let's Talk About APIs

Before we go further, you need to understand what an API is. I'll keep this simple.

What is an API?

API stands for Application Programming Interface. Think of it as a waiter at a restaurant.

  • You (the customer) want food from the kitchen

  • You can't walk into the kitchen yourself

  • The waiter takes your order, brings it to the kitchen, and returns with your food

An API works the same way:

  • Your script wants data from Airtable

  • Your script can't access Airtable's database directly

  • The API takes your request, gets the data, and returns it to your script

Every modern tool — Airtable, Ghost, Slack, Notion, Salesforce — has an API. That's how they talk to each other.

The Four Things You Can Do With an API

APIs let you do four basic operations (often called CRUD):

OPERATION

WHAT IT DOES

REAL EXAMPLE

Create

Add new data

Create a new member in Ghost

Read

Get existing data

Fetch all records from Airtable

Update

Change existing data

Mark a record as "Member = true"

Delete

Remove data

Delete a test account

In technical terms, these map to HTTP methods:

OPERATION

HTTP METHOD

WHAT YOUR SCRIPT SENDS

Create

POST

"Here's a new member, add them"

Read

GET

"Give me all the records"

Update

PATCH or PUT

"Change this field on this record"

Delete

DELETE

"Remove this record"

You don't need to memorize this. But when you see Claude's plan mention "GET request" or "POST request," now you know what it means.

API Keys: Our Script's Password

APIs need to know who's making the request. That's where API keys come in.

An API key is like a password that identifies your script. When your script talks to Airtable, it says: "Hey, it's me, here's my key, please give me access."

This is why API keys are secret. Anyone with your key can access your data. Never share them. Never put them in code that others can see.

Where API Keys Live: The .env File

For this project, I stored the API keys in a special file called .env (short for "environment"). It looks like this:




Your scripts read from this file, but the file itself never gets shared. This keeps your keys safe.

💡 Note: The .env file isn't just for API keys. You can use it to store any configuration your script needs to run — Base IDs, table names, URLs, and other settings all belong here. This makes your scripts reusable: someone else can use the same script with their own .env file, no code changes needed, because you're not "hard-coding" your API keys or personal data into the script itself.

Video 2: Script to Sync Existing Data

In this video, I build the entire first script from scratch. I show the flowchart, ask Claude to read the API docs, handle some dependency issues (pip vs pip3), and watch the records update at once.

Watch for these moments:

  • [~2:00] The flowchart explaining what Script 1 does

  • [~5:00] Asking Claude to read API documentation first

  • [~8:00] The .claude/settings.json file that stores permissions

  • [~15:00] Claude asking which language to use (Python)

  • [~20:00] The pip vs pip3 dependency issue

  • [~25:00] Running the sync and watching records update

What Script 1 Actually Does (Technical Breakdown)

You don't need to know how to code to build with Claude Code, but it's helpful to understand what's happening under the hood.

The Problem

I had 40 people already subscribed in Ghost (my newsletter platform). But my Airtable database was out of date and did not know about them — the "Member" checkbox was unchecked for everyone. I needed to sync this data.

The Solution: Three Steps




Step 1: Reading from Ghost (GET Request)

The script sends a GET request to Ghost's Admin API:




Ghost's Admin API uses JWT (JSON Web Token) authentication.

The script takes your Admin API key (which looks like key_id:secret), splits it apart, and generates a short-lived JWT token that expires in 5 minutes. This is more secure than sending the raw API key with each request.

Ghost responds with a list of all members — their emails, names, when they joined, etc. This data comes back as JSON, which looks like:

{
  "members": [
    {"email": "alice@example.com", "name": "Alice"},
    {"email": "bob@example.com", "name": "Bob"}
  ]
}
{
  "members": [
    {"email": "alice@example.com", "name": "Alice"},
    {"email": "bob@example.com", "name": "Bob"}
  ]
}
{
  "members": [
    {"email": "alice@example.com", "name": "Alice"},
    {"email": "bob@example.com", "name": "Bob"}
  ]
}

The script extracts just the emails: ["alice@example.com", "bob@example.com", ...]

Step 2: Reading from Airtable (GET Request)

Same idea. The script sends a GET request to Airtable:




Airtable responds with all records, including their email addresses and current "Member" status.

Step 3: Compare and Update (PATCH Request)

Now the script has two datasets:

  • Ghost members (40 emails)

  • Airtable records (91 records with emails and IDs)

It compares them: "Which Airtable records have emails that exist in Ghost?"

For each match, it sends a PATCH request to update that record:

PATCH https://api.airtable.com/v0/BASE_ID/TABLE_NAME
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "records": [
    {"id": "rec123abc", "fields": {"Member": true}},
    {"id": "rec456def", "fields": {"Member": true}}
  ]

PATCH https://api.airtable.com/v0/BASE_ID/TABLE_NAME
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "records": [
    {"id": "rec123abc", "fields": {"Member": true}},
    {"id": "rec456def", "fields": {"Member": true}}
  ]

PATCH https://api.airtable.com/v0/BASE_ID/TABLE_NAME
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "records": [
    {"id": "rec123abc", "fields": {"Member": true}},
    {"id": "rec456def", "fields": {"Member": true}}
  ]

This changes the "Member" checkbox from false to true.

Why Batching Matters: Airtable has rate limits — you can only update 10 records per request. If you try to update 36 records one at a time, you'll hit the limit and get errors. So the script batches: it groups records into sets of 10 and sends them together. That's why you see "Updating in batches of 10..." in the output.

The Upsert Optimization: After running the script once, I realized a problem: if I ran it again, it would try to update all 36 records again, even though they're already marked as members. That's wasteful. So I asked Claude to modify the script to only update records that need updating (and skip the ones already marked true). This makes the script idempotent — you can run it multiple times and it only changes what actually needs changing.

Now You Try

The specific tools don't matter — the pattern works for any two platforms with APIs. Here's the general approach:

Step 1: Set Up Your Environment File

Create a .env file with your API keys and configuration:




Step 2: Have Claude Learn the APIs

Before writing any code, point Claude to the documentation for each platform:

Read this documentation extensively and learn everything about 
the [Platform Name] API: [link to docs]
Read this documentation extensively and learn everything about 
the [Platform Name] API: [link to docs]
Read this documentation extensively and learn everything about 
the [Platform Name] API: [link to docs]

Step 3: Connect to Each Platform

Test each connection separately before trying to sync them:

Connect to my [Platform]

Connect to my [Platform]

Connect to my [Platform]

Step 4: Build the Sync Script

Use Plan Mode (Shift+Tab) and describe what you want:

Create a script that:
1. Fetches data from Platform A (GET)
2. Fetches data from Platform B (GET)
3. Compares them by [matching field, e.g., email]

Create a script that:
1. Fetches data from Platform A (GET)
2. Fetches data from Platform B (GET)
3. Compares them by [matching field, e.g., email]

Create a script that:
1. Fetches data from Platform A (GET)
2. Fetches data from Platform B (GET)
3. Compares them by [matching field, e.g., email]

Claude will ask clarifying questions about field names, filters, and preferences. Answer them, and let it build the script.

Video 3: Script to Create New Members

In this video, I build Script 2, which creates new members in Ghost based on approvals in Airtable. This video is longer because things go wrong — the magic link API returns 404 errors and Claude goes down some rabbit holes.

Watch for these moments:

  • [~3:00] The flowchart for Script 2 (different from Script 1)

  • [~10:00] Adding filters: only process records where Invite=true AND Member=false

  • [~20:00] The 404 error on the magic link endpoint

  • [~30:00] Claude trying multiple fixes that don't work

  • [~40:00] Finally getting it working

This video shows troubleshooting in real time. Things break. Claude doesn't always get it right the first time. Persistence matters.

What Script 2 Actually Does (Technical Breakdown)

Script 2 is more complex because it writes to Ghost, not just reads.

The Problem

New people request access via a form. I review them in Airtable and mark the ones I approve. But then I still have to manually create each account in Ghost.

The Solution: Three Steps




Step 1: Reading with Filters (GET Request)

We don't want all records — just the ones I've approved but haven't processed yet:




The filterByFormula parameter tells Airtable to only return matching records. The formula AND(NOT({Member}), {Invite}) means "Member is unchecked AND Invite is checked."

Step 2: Creating a Member with Automatic Email (POST Request)

For each approved person, the script sends a POST request to Ghost. Here's the key insight: Ghost can automatically send a welcome email when you create a member — you just need to include the right query parameters.

POST https://your-site.ghost.io/ghost/api/admin/members/?send_email=true&email_type=signup
Authorization: Ghost {JWT_TOKEN}
Content-Type: application/json

{
  "members": [{
    "email": "newperson@example.com",
    "name": "New Person",
    "labels": [
      {"name": "API", "slug": "api"},
      {"name": "Product Manager", "slug": "product-manager"}
    ]

POST https://your-site.ghost.io/ghost/api/admin/members/?send_email=true&email_type=signup
Authorization: Ghost {JWT_TOKEN}
Content-Type: application/json

{
  "members": [{
    "email": "newperson@example.com",
    "name": "New Person",
    "labels": [
      {"name": "API", "slug": "api"},
      {"name": "Product Manager", "slug": "product-manager"}
    ]

POST https://your-site.ghost.io/ghost/api/admin/members/?send_email=true&email_type=signup
Authorization: Ghost {JWT_TOKEN}
Content-Type: application/json

{
  "members": [{
    "email": "newperson@example.com",
    "name": "New Person",
    "labels": [
      {"name": "API", "slug": "api"},
      {"name": "Product Manager", "slug": "product-manager"}
    ]

The magic is in those query parameters:

  • send_email=true — tells Ghost to email the new member

  • email_type=signup — sends the signup/welcome email template

One request, two things happen: member created and email sent.

Note on labels: Ghost expects labels as objects with both name and slug fields, not just strings. The script always adds an "API" label to track members created via automation, plus their role from Airtable.

Step 3: Updating Airtable (PATCH Request)

Finally, we mark records as processed so we don't create duplicate accounts next time:

PATCH https://api.airtable.com/v0/BASE_ID/TABLE_NAME
Authorization: Bearer YOUR_API_KEY

{
  "records": [
    {"id": "rec123abc", "fields": {"Member": true}},
    {"id": "rec456def", "fields": {"Member": true}}
  ]

PATCH https://api.airtable.com/v0/BASE_ID/TABLE_NAME
Authorization: Bearer YOUR_API_KEY

{
  "records": [
    {"id": "rec123abc", "fields": {"Member": true}},
    {"id": "rec456def", "fields": {"Member": true}}
  ]

PATCH https://api.airtable.com/v0/BASE_ID/TABLE_NAME
Authorization: Bearer YOUR_API_KEY

{
  "records": [
    {"id": "rec123abc", "fields": {"Member": true}},
    {"id": "rec456def", "fields": {"Member": true}}
  ]

Like in the first script, updates are batched in groups of 10 to respect Airtable's rate limits.

The Complete Flow

For each approved record:

  1. Create member in Ghost

  2. If successful, add to "success" list

  3. If failed, log error, continue to next record

  4. After all records processed, mark successful records as Member=true in Airtable

When Things Go Wrong: The 404 Saga

In the latest video, you watched me hit a wall. I was trying to send welcome emails to new members, but my approach wasn't working.

What I Tried First (The Wrong Approach)

I initially thought I needed two separate API calls:

  1. POST to create the member

  2. POST to a "magic link" endpoint to send the email

The second call kept failing:




Claude tried several fixes — different endpoint URLs, different headers, different payloads. Nothing worked.

What Actually Fixed It

I knew it was possible — I'd gotten it working that morning with a different script. But right as I was preparing to stop the troubleshooting process, Claude figured it out on its own. It created a test member — and the email came through.

The solution wasn't to fix the magic link endpoint — it was to realize I didn't need it at all.

When Claude gets stuck, here's my advice:
1. Don't give up after one failure. It often needs a few attempts.
2. Give it one more chance. Sometimes Claude finds the answer right when you're about to lose faith.
3. Go back to the source. "Read the docs again" often reveals a simpler approach.
4. Share what you know. "I know this is possible" gives Claude useful context.
5. Look for simpler solutions. Sometimes the fix isn't debugging your complex approach — it's finding a simpler one.

Now You Try

The second script is about creating new records and triggering actions when you do.

The Pattern

Most "approval workflow" automations follow this structure:




The Prompt Template

Use Plan Mode (Shift+Tab) and adapt this to your tools:

Create a script that:

1. Fetches records from [Platform A] where [approval field] = true 
   AND [processed field] = false (GET with filters)
2. For each record:
   - Creates a new [thing] in [Platform B] (POST) with:
     - [Field mapping: which fields go where]
   - Triggers [any automatic actions, like sending an email]
3. Updates [Platform A]

Create a script that:

1. Fetches records from [Platform A] where [approval field] = true 
   AND [processed field] = false (GET with filters)
2. For each record:
   - Creates a new [thing] in [Platform B] (POST) with:
     - [Field mapping: which fields go where]
   - Triggers [any automatic actions, like sending an email]
3. Updates [Platform A]

Create a script that:

1. Fetches records from [Platform A] where [approval field] = true 
   AND [processed field] = false (GET with filters)
2. For each record:
   - Creates a new [thing] in [Platform B] (POST) with:
     - [Field mapping: which fields go where]
   - Triggers [any automatic actions, like sending an email]
3. Updates [Platform A]

Key Questions Claude Will Ask

  • What are the filter conditions? (e.g., "Approved = true AND Created = false")

  • What fields should map to what? (e.g., "Email from column A, Name from column B")

  • What should happen when a record fails? (Skip and continue, or stop entirely?)

  • Should it ask for confirmation? (Yes, especially while you're learning)

Pro Tip: Check the Docs for Bonus Features

Many APIs can do more than just create a record. Ghost, for example, can send a welcome email automatically if you add the right parameters to your POST request. Stripe can send receipts. Notion can notify users.

Before building, ask Claude:

What optional parameters does [Platform B]'s API support 
when creating a [thing]

What optional parameters does [Platform B]'s API support 
when creating a [thing]

What optional parameters does [Platform B]'s API support 
when creating a [thing]

You might save yourself an extra API call 😄

You did it!

WHere next?

Choose your own adventure

Join the teams who rely on Plain to provide world-class support