Developers

Connect AutoMarketer to the tools you already use

A REST API and signed webhooks, included in every paid plan. Pull approved articles into your CMS, send new ads to a spreadsheet, approve from Slack, start a cycle from your own script, or wire it all up in Zapier, Make or n8n without code.

Overview

Authentication

Plans and limits

Errors

Pagination

Endpoints

In the examples, $AUTOMARKETER_API_KEY holds your key.

GET /me scope: read

Your team, plan, limits and usage

Use this first to check that a key works. It returns the team the key belongs to, the plan with its limits, and how many assets were generated this calendar month.

curl https://automarketer.com/api/v1/me \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "team": {
        "id": 87,
        "name": "Your Website"
    },
    "plan": {
        "key": "growth",
        "name": "Growth",
        "mode": "autopilot",
        "frequency": "daily",
        "channels": [
            "seo",
            "social",
            "email",
            "google_ads"
        ],
        "limits": {
            "assets_per_month": 150,
            "sites": 1,
            "seats": 3
        }
    },
    "usage": {
        "assets_this_month": 42,
        "assets_remaining": 108,
        "sites": 1,
        "month_resets_at": "2026-11-01T00:00:00Z"
    },
    "api_key": {
        "id": 12,
        "name": "Zapier",
        "scopes": [
            "read",
            "write"
        ],
        "created_at": "2026-10-07T08:00:00Z"
    },
    "rate_limit": {
        "requests_per_minute": 60
    }
}
GET /websites scope: read

List your websites

Newest first, 25 per page. Pass per_page (up to 100) and page.

curl https://automarketer.com/api/v1/websites \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "data": [
        {
            "id": 311,
            "object": "website",
            "url": "https://yourwebsite.com",
            "host": "yourwebsite.com",
            "name": "Your Website",
            "status": "ready",
            "failure_reason": null,
            "autopilot": {
                "on": true,
                "mode": "copilot",
                "frequency": "daily",
                "last_run_at": "2026-10-07T06:00:12Z",
                "next_run_at": "2026-10-08T06:00:12Z"
            },
            "created_at": "2026-09-30T14:02:51Z",
            "updated_at": "2026-10-07T06:00:12Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    },
    "links": {
        "next": null,
        "prev": null
    }
}
POST /websites scope: write

Add a website and start its analysis

The same as adding a site in the cockpit. It takes one of the sites in your plan and starts the analysis that builds the brand profile, keywords and plan, which takes about a minute. Poll GET /websites/{website} until status is ready, or listen for the website.analyzed webhook.

curl -X POST https://automarketer.com/api/v1/websites \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "yourwebsite.com"}'

Response 202

{
    "id": 311,
    "object": "website",
    "url": "https://yourwebsite.com",
    "host": "yourwebsite.com",
    "name": "Your Website",
    "status": "analyzing",
    "failure_reason": null,
    "autopilot": {
        "on": false,
        "mode": "copilot",
        "frequency": "weekly",
        "last_run_at": null,
        "next_run_at": null
    },
    "created_at": "2026-09-30T14:02:51Z",
    "updated_at": "2026-10-07T06:00:12Z",
    "brand_profile": null,
    "keywords": null,
    "plan": null
}
  • 403 site_limit when every site in your plan is in use.
  • 429 analysis_limit after 30 analyses in an hour.
GET /websites/{website} scope: read

One website in full

Adds the brand profile, the keywords (primary keyword, keyword groups and negative keywords, null until the analysis is ready), the marketing plan and the six channels.

curl https://automarketer.com/api/v1/websites/311 \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "id": 311,
    "object": "website",
    "url": "https://yourwebsite.com",
    "host": "yourwebsite.com",
    "name": "Your Website",
    "status": "ready",
    "failure_reason": null,
    "autopilot": {
        "on": true,
        "mode": "copilot",
        "frequency": "daily",
        "last_run_at": "2026-10-07T06:00:12Z",
        "next_run_at": "2026-10-08T06:00:12Z"
    },
    "created_at": "2026-09-30T14:02:51Z",
    "updated_at": "2026-10-07T06:00:12Z",
    "brand_profile": {
        "name": "Your Website",
        "what": "Online bookkeeping for small agencies",
        "audience": "Agency owners with 2 to 20 people",
        "tone": "Plain and practical"
    },
    "keywords": {
        "primary": "bookkeeping for agencies",
        "groups": [
            {
                "theme": "Core services",
                "keywords": [
                    {
                        "keyword": "bookkeeping for agencies",
                        "intent": "commercial",
                        "volume_hint": "Medium",
                        "difficulty_hint": "Low"
                    }
                ]
            }
        ],
        "negatives": [
            "free",
            "jobs"
        ]
    },
    "plan": {
        "summary": "Lead with search, support with weekly articles."
    },
    "channels": [
        {
            "type": "google_ads",
            "label": "Google Ads",
            "enabled": true,
            "mode": "copilot",
            "in_plan": true,
            "account_label": "123-456-7890",
            "daily_budget": 25,
            "last_generated_at": "2026-10-07T06:01:40Z"
        }
    ]
}
PATCH /websites/{website} scope: write

Switch the autopilot on or off, change its mode or cadence

The same as the autopilot switch, Mode and Cadence on the dashboard. Send autopilot (true or false), mode (copilot or autopilot), frequency (weekly, daily or continuous), or several. Your plan decides as in the cockpit: autopilot mode starts at Growth, each plan has its cadence, and the autopilot switches on only while this month's assets last. Switching off is always allowed. The response is the website in full.

curl -X PATCH https://automarketer.com/api/v1/websites/311 \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"autopilot": true, "frequency": "daily"}'

Response 200

{
    "id": 311,
    "object": "website",
    "url": "https://yourwebsite.com",
    "host": "yourwebsite.com",
    "name": "Your Website",
    "status": "ready",
    "failure_reason": null,
    "autopilot": {
        "on": true,
        "mode": "copilot",
        "frequency": "daily",
        "last_run_at": "2026-10-07T06:00:12Z",
        "next_run_at": "2026-10-07T09:12:40Z"
    },
    "created_at": "2026-09-30T14:02:51Z",
    "updated_at": "2026-10-07T06:00:12Z",
    "brand_profile": {
        "name": "Your Website",
        "what": "Online bookkeeping for small agencies",
        "audience": "Agency owners with 2 to 20 people",
        "tone": "Plain and practical"
    },
    "keywords": {
        "primary": "bookkeeping for agencies",
        "groups": [
            {
                "theme": "Core services",
                "keywords": [
                    {
                        "keyword": "bookkeeping for agencies",
                        "intent": "commercial",
                        "volume_hint": "Medium",
                        "difficulty_hint": "Low"
                    }
                ]
            }
        ],
        "negatives": [
            "free",
            "jobs"
        ]
    },
    "plan": {
        "summary": "Lead with search, support with weekly articles."
    },
    "channels": [
        {
            "type": "google_ads",
            "label": "Google Ads",
            "enabled": true,
            "mode": "copilot",
            "in_plan": true,
            "account_label": "123-456-7890",
            "daily_budget": 25,
            "last_generated_at": "2026-10-07T06:01:40Z"
        }
    ]
}
  • 403 mode_not_in_plan, frequency_not_in_plan or asset_limit, with upgrade_url.
POST /websites/{website}/analyze scope: write

Analyze a website again

The same as Re-analyze in the cockpit: the brand profile, keywords and plan are built again from the live website, which takes about a minute. Listen for website.analyzed or poll GET /websites/{website}.

curl -X POST https://automarketer.com/api/v1/websites/311/analyze \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 202

{
    "id": 311,
    "object": "website",
    "url": "https://yourwebsite.com",
    "host": "yourwebsite.com",
    "name": "Your Website",
    "status": "analyzing",
    "failure_reason": null,
    "autopilot": {
        "on": true,
        "mode": "copilot",
        "frequency": "daily",
        "last_run_at": "2026-10-07T06:00:12Z",
        "next_run_at": "2026-10-08T06:00:12Z"
    },
    "created_at": "2026-09-30T14:02:51Z",
    "updated_at": "2026-10-07T06:00:12Z",
    "brand_profile": {
        "name": "Your Website",
        "what": "Online bookkeeping for small agencies",
        "audience": "Agency owners with 2 to 20 people",
        "tone": "Plain and practical"
    },
    "keywords": {
        "primary": "bookkeeping for agencies",
        "groups": [
            {
                "theme": "Core services",
                "keywords": [
                    {
                        "keyword": "bookkeeping for agencies",
                        "intent": "commercial",
                        "volume_hint": "Medium",
                        "difficulty_hint": "Low"
                    }
                ]
            }
        ],
        "negatives": [
            "free",
            "jobs"
        ]
    },
    "plan": {
        "summary": "Lead with search, support with weekly articles."
    },
    "channels": [
        {
            "type": "google_ads",
            "label": "Google Ads",
            "enabled": true,
            "mode": "copilot",
            "in_plan": true,
            "account_label": "123-456-7890",
            "daily_budget": 25,
            "last_generated_at": "2026-10-07T06:01:40Z"
        }
    ]
}
  • 409 website_not_ready while an analysis is already running.
  • 429 analysis_limit after 30 analyses in an hour, shared with the cockpit.
GET /websites/{website}/channels scope: read

The six channels of a website

seo, google_ads, bing_ads, meta_ads, social and email, each with whether it is on, its mode and whether your plan includes it.

curl https://automarketer.com/api/v1/websites/311/channels \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "data": [
        {
            "type": "google_ads",
            "label": "Google Ads",
            "enabled": true,
            "mode": "copilot",
            "in_plan": true,
            "account_label": "123-456-7890",
            "daily_budget": 25,
            "last_generated_at": "2026-10-07T06:01:40Z"
        }
    ]
}
PATCH /websites/{website}/channels/{type} scope: write

Switch a channel on or off, or change its mode

Send enabled (true or false), mode (copilot or autopilot), or both. Switching a channel on and choosing autopilot follow your plan, exactly as in the cockpit. Switching off is always allowed.

curl -X PATCH https://automarketer.com/api/v1/websites/311/channels/google_ads \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

Response 200

{
    "type": "google_ads",
    "label": "Google Ads",
    "enabled": true,
    "mode": "copilot",
    "in_plan": true,
    "account_label": "123-456-7890",
    "daily_budget": 25,
    "last_generated_at": "2026-10-07T06:01:40Z"
}
  • 403 channel_not_in_plan or mode_not_in_plan, with upgrade_url.
  • 409 website_not_ready while the analysis is still running.
GET /assets scope: read

List assets

Newest first. Filters: website_id, channel (seo, google_ads, bing_ads, meta_ads, social, email), status (pending_approval, approved, rejected and the rest), type (article, seo_audit, keyword_plan, google_rsa, bing_rsa, meta_ad, social_post, email), since and updated_since (ISO 8601). To poll for new work, keep the newest created_at you have seen and pass it as since.

curl "https://automarketer.com/api/v1/assets?status=pending_approval&channel=google_ads&since=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "data": [
        {
            "id": 4812,
            "object": "asset",
            "website_id": 311,
            "channel": "google_ads",
            "type": "google_rsa",
            "type_label": "Google Search Ad",
            "status": "pending_approval",
            "title": "Bookkeeping for agencies",
            "body": null,
            "payload": {
                "ad_group": "Bookkeeping for agencies",
                "headlines": [
                    "Bookkeeping For Agencies",
                    "Books Closed Every Month",
                    "From $49 a Month"
                ],
                "descriptions": [
                    "Monthly books, payroll and reports for agencies. Start in a day.",
                    "A bookkeeper who knows retainers and project billing."
                ],
                "final_url": "https://yourwebsite.com/pricing",
                "keywords": [
                    {
                        "keyword": "agency bookkeeping",
                        "match": "phrase"
                    }
                ]
            },
            "scheduled_for": null,
            "is_sample": false,
            "run_id": 905,
            "exports": [
                {
                    "format": "csv",
                    "url": "https://automarketer.com/api/v1/assets/4812/export?format=csv"
                }
            ],
            "created_at": "2026-10-07T06:01:40Z",
            "updated_at": "2026-10-07T06:01:40Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    },
    "links": {
        "next": null,
        "prev": null
    }
}
GET /assets/{asset} scope: read

One asset with its full content

title and body hold the main text. payload holds the fields of the asset type: headlines, descriptions and keywords of an ad, meta title and description of an article, subject and HTML of an email, platform, hashtags and scheduled time of a social post.

curl https://automarketer.com/api/v1/assets/4812 \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "id": 4812,
    "object": "asset",
    "website_id": 311,
    "channel": "google_ads",
    "type": "google_rsa",
    "type_label": "Google Search Ad",
    "status": "pending_approval",
    "title": "Bookkeeping for agencies",
    "body": null,
    "payload": {
        "ad_group": "Bookkeeping for agencies",
        "headlines": [
            "Bookkeeping For Agencies",
            "Books Closed Every Month",
            "From $49 a Month"
        ],
        "descriptions": [
            "Monthly books, payroll and reports for agencies. Start in a day.",
            "A bookkeeper who knows retainers and project billing."
        ],
        "final_url": "https://yourwebsite.com/pricing",
        "keywords": [
            {
                "keyword": "agency bookkeeping",
                "match": "phrase"
            }
        ]
    },
    "scheduled_for": null,
    "is_sample": false,
    "run_id": 905,
    "exports": [
        {
            "format": "csv",
            "url": "https://automarketer.com/api/v1/assets/4812/export?format=csv"
        }
    ],
    "created_at": "2026-10-07T06:01:40Z",
    "updated_at": "2026-10-07T06:01:40Z",
    "editable_fields": [
        {
            "key": "payload__headlines",
            "label": "Headlines",
            "kind": "lines",
            "value": [
                "Bookkeeping For Agencies",
                "Books Closed Every Month",
                "From $49 a Month"
            ]
        },
        {
            "key": "payload__descriptions",
            "label": "Descriptions",
            "kind": "lines",
            "value": [
                "Monthly books, payroll and reports for agencies. Start in a day.",
                "A bookkeeper who knows retainers and project billing."
            ]
        },
        {
            "key": "payload__final_url",
            "label": "Final URL",
            "kind": "text",
            "value": "https://yourwebsite.com/pricing"
        }
    ]
}
PATCH /assets/{asset} scope: write

Edit an asset

The same as Edit in the Asset Queue. Send title, fields, or both. The keys of fields are the editable_fields of GET /assets/{asset}: a string for kind text and textarea, a list of strings for kind lines. The export files change with the edit, and the asset.updated webhook is sent. The status does not change.

curl -X PATCH https://automarketer.com/api/v1/assets/4812 \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fields": {"payload__headlines": ["Bookkeeping For Agencies", "Books Closed Every Month", "Plans From $49 a Month"]}}'

Response 200

{
    "id": 4812,
    "object": "asset",
    "website_id": 311,
    "channel": "google_ads",
    "type": "google_rsa",
    "type_label": "Google Search Ad",
    "status": "pending_approval",
    "title": "Bookkeeping for agencies",
    "body": null,
    "payload": {
        "ad_group": "Bookkeeping for agencies",
        "headlines": [
            "Bookkeeping For Agencies",
            "Books Closed Every Month",
            "Plans From $49 a Month"
        ],
        "descriptions": [
            "Monthly books, payroll and reports for agencies. Start in a day.",
            "A bookkeeper who knows retainers and project billing."
        ],
        "final_url": "https://yourwebsite.com/pricing",
        "keywords": [
            {
                "keyword": "agency bookkeeping",
                "match": "phrase"
            }
        ]
    },
    "scheduled_for": null,
    "is_sample": false,
    "run_id": 905,
    "exports": [
        {
            "format": "csv",
            "url": "https://automarketer.com/api/v1/assets/4812/export?format=csv"
        }
    ],
    "created_at": "2026-10-07T06:01:40Z",
    "updated_at": "2026-10-07T06:01:40Z",
    "editable_fields": [
        {
            "key": "payload__headlines",
            "label": "Headlines",
            "kind": "lines",
            "value": [
                "Bookkeeping For Agencies",
                "Books Closed Every Month",
                "Plans From $49 a Month"
            ]
        },
        {
            "key": "payload__descriptions",
            "label": "Descriptions",
            "kind": "lines",
            "value": [
                "Monthly books, payroll and reports for agencies. Start in a day.",
                "A bookkeeper who knows retainers and project billing."
            ]
        },
        {
            "key": "payload__final_url",
            "label": "Final URL",
            "kind": "text",
            "value": "https://yourwebsite.com/pricing"
        }
    ]
}
  • 422 validation_failed for a key that is not an editable field of this asset, with the list in editable_fields.
DELETE /assets/{asset} scope: write

Delete an asset

The same as Delete in the Asset Queue: the content is erased and the asset leaves the queue and every export. It still counts toward this month's assets, because it was generated. The asset.deleted webhook is sent.

curl -X DELETE https://automarketer.com/api/v1/assets/4812 \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "id": 4812,
    "object": "asset",
    "deleted": true
}
POST /assets/{asset}/approve scope: write

Approve an asset

The same as Approve in the Asset Queue. The approval also counts towards which channels the next cycle works first.

curl -X POST https://automarketer.com/api/v1/assets/4812/approve \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "id": 4812,
    "object": "asset",
    "website_id": 311,
    "channel": "google_ads",
    "type": "google_rsa",
    "type_label": "Google Search Ad",
    "status": "approved",
    "title": "Bookkeeping for agencies",
    "body": null,
    "payload": {
        "ad_group": "Bookkeeping for agencies",
        "headlines": [
            "Bookkeeping For Agencies",
            "Books Closed Every Month",
            "From $49 a Month"
        ],
        "descriptions": [
            "Monthly books, payroll and reports for agencies. Start in a day.",
            "A bookkeeper who knows retainers and project billing."
        ],
        "final_url": "https://yourwebsite.com/pricing",
        "keywords": [
            {
                "keyword": "agency bookkeeping",
                "match": "phrase"
            }
        ]
    },
    "scheduled_for": null,
    "is_sample": false,
    "run_id": 905,
    "exports": [
        {
            "format": "csv",
            "url": "https://automarketer.com/api/v1/assets/4812/export?format=csv"
        }
    ],
    "created_at": "2026-10-07T06:01:40Z",
    "updated_at": "2026-10-07T06:01:40Z"
}
POST /assets/{asset}/reject scope: write

Reject an asset

The same as Reject in the Asset Queue. An optional reason (up to 1,000 characters) is saved with the decision and sent in the asset.rejected webhook. A rejected asset leaves the context the next cycle is written from.

curl -X POST https://automarketer.com/api/v1/assets/4812/reject \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Too salesy for our audience"}'

Response 200

{
    "id": 4812,
    "object": "asset",
    "website_id": 311,
    "channel": "google_ads",
    "type": "google_rsa",
    "type_label": "Google Search Ad",
    "status": "rejected",
    "title": "Bookkeeping for agencies",
    "body": null,
    "payload": {
        "ad_group": "Bookkeeping for agencies",
        "headlines": [
            "Bookkeeping For Agencies",
            "Books Closed Every Month",
            "From $49 a Month"
        ],
        "descriptions": [
            "Monthly books, payroll and reports for agencies. Start in a day.",
            "A bookkeeper who knows retainers and project billing."
        ],
        "final_url": "https://yourwebsite.com/pricing",
        "keywords": [
            {
                "keyword": "agency bookkeeping",
                "match": "phrase"
            }
        ]
    },
    "scheduled_for": null,
    "is_sample": false,
    "run_id": 905,
    "exports": [
        {
            "format": "csv",
            "url": "https://automarketer.com/api/v1/assets/4812/export?format=csv"
        }
    ],
    "created_at": "2026-10-07T06:01:40Z",
    "updated_at": "2026-10-07T06:01:40Z",
    "rejection_reason": "Too salesy for our audience"
}
GET /assets/{asset}/export scope: read

Download the export file

The same file as the export buttons in the cockpit. format: csv for google_rsa (Google Ads Editor import) and bing_rsa (Microsoft Advertising bulk import), html or markdown for article, html for email, csv for one social_post. Without format you get the first one in that list.

curl -OJ "https://automarketer.com/api/v1/assets/4812/export?format=csv" \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200: the file, with Content-Disposition carrying its file name.

  • 422 not_exportable for a type without a file (seo_audit, keyword_plan, meta_ad) and for an ad below 3 headlines and 2 descriptions.
GET /websites/{website}/export/social scope: read

Social calendar CSV for one network

Every post that is not rejected, for one network, in scheduled order. network: instagram, facebook, linkedin, x or pinterest.

curl -OJ "https://automarketer.com/api/v1/websites/311/export/social?network=linkedin" \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200: the file, with Content-Disposition carrying its file name.

POST /websites/{website}/runs scope: write

Run one cycle now

The same as Run a cycle now on the dashboard: one cycle for the channels that are on, within your monthly asset allowance. The autopilot switch is not changed. Poll GET /runs/{run} or listen for run.completed.

curl -X POST https://automarketer.com/api/v1/websites/311/runs \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 202

{
    "id": 905,
    "object": "run",
    "website_id": 311,
    "status": "queued",
    "trigger": "api",
    "summary": null,
    "assets_created": 0,
    "channels_touched": [],
    "started_at": null,
    "finished_at": null,
    "asset_ids": []
}
  • 403 asset_limit when this month's assets are used.
  • 409 run_in_progress while a cycle for this website is still running, and website_not_ready during analysis.
GET /runs/{run} scope: read

One cycle

status is queued, running, success, partial, skipped or failed. asset_ids lists what the cycle created.

curl https://automarketer.com/api/v1/runs/905 \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "id": 905,
    "object": "run",
    "website_id": 311,
    "status": "success",
    "trigger": "api",
    "summary": "This cycle I generated 6 assets across Google Ads and SEO & Content.",
    "assets_created": 6,
    "channels_touched": [
        "google_ads",
        "seo"
    ],
    "started_at": "2026-10-07T06:00:12Z",
    "finished_at": "2026-10-07T06:03:02Z",
    "asset_ids": [
        4807,
        4808,
        4809,
        4810,
        4811,
        4812
    ]
}
GET /websites/{website}/runs scope: read

Cycles of a website

Newest first, paginated like every list.

curl https://automarketer.com/api/v1/websites/311/runs \
  -H "Authorization: Bearer $AUTOMARKETER_API_KEY"

Response 200

{
    "data": [
        {
            "id": 905,
            "object": "run",
            "website_id": 311,
            "status": "success",
            "trigger": "api",
            "summary": "This cycle I generated 6 assets across Google Ads and SEO & Content.",
            "assets_created": 6,
            "channels_touched": [
                "google_ads",
                "seo"
            ],
            "started_at": "2026-10-07T06:00:12Z",
            "finished_at": "2026-10-07T06:03:02Z"
        }
    ],
    "meta": {
        "current_page": 1,
        "per_page": 25,
        "total": 1,
        "last_page": 1
    },
    "links": {
        "next": null,
        "prev": null
    }
}

Webhooks

Verifying signatures

PHP

<?php
$secret = getenv('AUTOMARKETER_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_AUTOMARKETER_SIGNATURE'] ?? '';

parse_str(str_replace(',', '&', $header), $parts);
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);

$fresh = abs(time() - $timestamp) <= 300;
$valid = $fresh && hash_equals($expected, (string) ($parts['v1'] ?? ''));

if ($valid === false) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
// $event['type'] is for example "asset.approved", $event['data']['asset'] the asset
http_response_code(200);

Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

// Keep the raw body: the signature is computed over the exact bytes we sent.
app.post('/automarketer', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = process.env.AUTOMARKETER_WEBHOOK_SECRET;
  const header = req.get('X-AutoMarketer-Signature') || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + req.body.toString('utf8'))
    .digest('hex');

  const given = Buffer.from(parts.v1 || '', 'utf8');
  const fresh = Math.abs(Date.now() / 1000 - timestamp) <= 300;
  const valid = fresh && given.length === expected.length
    && crypto.timingSafeEqual(given, Buffer.from(expected, 'utf8'));

  if (valid === false) return res.sendStatus(400);

  const event = JSON.parse(req.body.toString('utf8'));
  // event.type, event.data.asset
  res.sendStatus(200);
});

app.listen(3000);

Python (Flask)

import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/automarketer")
def automarketer():
    secret = os.environ["AUTOMARKETER_WEBHOOK_SECRET"].encode()
    body = request.get_data()  # raw bytes, before any JSON parsing
    header = request.headers.get("X-AutoMarketer-Signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    timestamp = parts.get("t", "")
    if not timestamp.isdigit():
        abort(400)

    expected = hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(timestamp)) <= 300
    if not (fresh and hmac.compare_digest(expected, parts.get("v1", ""))):
        abort(400)

    event = request.get_json()
    # event["type"], event["data"]["asset"]
    return "", 200

Connect with Zapier, Make or n8n

OpenAPI file