Skip to main content

Help / For Promoters

Promoter Events API

Send your events to Meet Me at the Fair straight from your own website, ticketing system, Zapier or Make. Listing is free.

New events are reviewed before they appear. A new event goes in as pending and a person checks it. A change to an event that is already listed, and a cancellation, are also reviewed before they go live. The API never changes a listed event directly.

1. Get a key

Claim your promoter listing, then open Promoter Portal → API Keys and create a key. You get a key ID (starts with mmk_) and a secret. The secret is shown once. Keep it on your server or in your automation tool's secret settings, never in a web page. Revoke a key there at any time; it stops working at once.

2. Sign every request

Every request, including GET, carries four headers:

  • X-MMATF-Key: your key ID.
  • X-MMATF-Timestamp: the current Unix time in seconds. It must be within 300 seconds of our clock.
  • X-MMATF-Request-Id: a new unique value for every request (a UUID is ideal), 1–100 printable characters with no spaces. A request id can be used once per key.
  • X-MMATF-Signature: sha256= followed by the hex HMAC-SHA256 of <timestamp>.<request id>.<raw body>, keyed with your secret. The body is the exact bytes you send; for GET it is empty, so you sign <timestamp>.<request id>.

curl

KEY_ID="mmk_your_key_id"
SECRET="your_secret"
BODY='{"external_id":"fall-fair-2027","name":"Fall Craft Fair","start_date":"2027-10-02","end_date":"2027-10-03","daily_hours":"9am-4pm","venue_name":"Town Hall","city":"Concord","state":"NH","admission":"Free"}'
TS=$(date +%s)
REQ=$(uuidgen)
SIG=$(printf '%s' "$TS.$REQ.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -sS -X POST https://meetmeatthefair.com/api/v1/promoter/events \
  -H "Content-Type: application/json" \
  -H "X-MMATF-Key: $KEY_ID" \
  -H "X-MMATF-Timestamp: $TS" \
  -H "X-MMATF-Request-Id: $REQ" \
  -H "X-MMATF-Signature: sha256=$SIG" \
  --data-binary "$BODY"

Node.js

import { createHmac, randomUUID } from "node:crypto";

const KEY_ID = process.env.MMATF_KEY_ID;   // mmk_...
const SECRET = process.env.MMATF_SECRET;   // never in a web page

async function mmatf(method, path, payload) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const ts = String(Math.floor(Date.now() / 1000));
  const requestId = randomUUID();
  const sig = createHmac("sha256", SECRET).update(`${ts}.${requestId}.${body}`).digest("hex");
  const res = await fetch(`https://meetmeatthefair.com${path}`, {
    method,
    headers: {
      "Content-Type": "application/json",
      "X-MMATF-Key": KEY_ID,
      "X-MMATF-Timestamp": ts,
      "X-MMATF-Request-Id": requestId,
      "X-MMATF-Signature": `sha256=${sig}`,
    },
    body: body || undefined,
  });
  return { status: res.status, json: await res.json() };
}

// Create or update by your own id
await mmatf("POST", "/api/v1/promoter/events", {
  external_id: "fall-fair-2027",
  name: "Fall Craft Fair",
  start_date: "2027-10-02",
  city: "Concord",
  state: "NH",
});
// Change one field (staged for review)
await mmatf("PATCH", "/api/v1/promoter/events/fall-fair-2027", { daily_hours: "10am-5pm" });
// Ask for a cancellation (staged for review)
await mmatf("POST", "/api/v1/promoter/events/fall-fair-2027/cancel", { reason: "Rain date moved" });
// List your events
await mmatf("GET", "/api/v1/promoter/events?limit=50");

3. Endpoints

POST /api/v1/promoter/events

Create or update one event, keyed by external_id: your own id for the event (letters, digits and . _ : ~ -, up to 100 characters). Posting the same external_id again never makes a second event.

  • A new external_id creates a pending event: 201 {outcome: "created", status: "PENDING"}. If it matches an event you already have with us, it is linked to that event instead.
  • A known external_id stages your changes for review: outcome: "update_staged" with changes, or "unchanged" if nothing differs.
  • A blank or null field proposes nothing. The API never clears a field.

PATCH /api/v1/promoter/events/{external_id}

Change only the fields you send. Staged for review. 404 if the id is unknown.

POST /api/v1/promoter/events/{external_id}/cancel

Ask for a cancellation, with an optional {"reason": "…"}. We review it before the event is marked cancelled: outcome: "cancel_staged".

GET /api/v1/promoter/events?limit=50&cursor=…

Your external ids with each event's status, ordered by external id. Pass next_cursor back as cursor for the next page.

The machine-readable contract is at /api/v1/openapi.json (OpenAPI 3.1). Each key may make 60 requests a minute.

Test without saving

Add ?dry_run=true to the URL, or "dry_run": true to the JSON body, on POST /api/v1/promoter/events, PATCH and /cancel. Sign the request exactly as usual. We check it the same way and save nothing: no event, no link to your external_id, nothing for review.

A dry run answers 200 with "dry_run": true, would_return (the status and outcome the real request would get) and a report:

  • read: each field as we read it.
  • outcome: new (would go to review), match (your listing, with the fields that would change, old and new), or skip with the reason.
  • problems: missing or unreadable fields, a date that has passed, an incomplete location, or a start time that looks like AM when the description says PM.

A body with an unknown field or a wrong type is still a 422. The request id is used up by a dry run too, so send a new X-MMATF-Request-Id for the real request.

Fields

The same fields as the spreadsheet import, as JSON. Values may be strings or numbers; categories may also be a list.

FieldRequired to createExample
nameYesFall Craft Fair
start_dateYes2027-10-02
end_date2027-10-03
daily_hours9am-4pm
venue_nameTown Hall
address12 Main St
cityYesConcord
stateYesNH
zip03301
description60 juried artisans, food and music.
categoriesCraft Fair; Holiday Market
admissionFree
vendor_application_urlhttps://example.org/apply
application_deadline2027-08-15
booth_fee_min75
booth_fee_max125
event_urlhttps://example.org/fall-fair
image_urlhttps://example.org/poster.jpg

On an existing event, changes to hours, venue, address, city, state, ZIP and categories are not compared yet; the response lists them under not_compared. Email us those changes for now.

Errors

Every error is {"error": {"code", "message", "errors"?}}. A validation error lists each problem: [{"field": "start_date", "message": "…"}].

HTTPcodeMeaning
401missing_auth_headersA signing header is missing or malformed.
401invalid_keyThe key is unknown or revoked.
401stale_timestampX-MMATF-Timestamp is more than 300 seconds from our clock. Check the sender's clock.
401bad_signatureThe signature does not match. See the signing recipe.
409replayed_requestThis request id was already used with this key. Send a new one.
409conflict_other_promoterThe event matches one another organizer lists. Email us if it is yours.
409conflict_external_idThe event matches one of yours already linked to a different external_id.
409in_progressAnother request for this external_id is still running. Retry shortly.
404not_foundNo event with that external_id for your key.
413body_too_largeThe body is over 64 KB.
400invalid_jsonThe body is not JSON.
422validation_failederror.errors lists each problem as {field, message}.
429rate_limitedMore than 60 requests a minute for this key. Wait for Retry-After.
500internal_errorOur side failed. Retry with a new request id.

Zapier recipe

  1. Trigger: wherever your events live (a Google Sheet row, a form, your calendar).
  2. Action Code by Zapier → Run JavaScript. Add the Input Data fields below (paste your secret as secret) and this code:
// Code by Zapier → Run JavaScript.
// Input Data: secret, external_id, name, start_date, city, state (map from your trigger)
const crypto = require("crypto");
const body = JSON.stringify({
  external_id: inputData.external_id,
  name: inputData.name,
  start_date: inputData.start_date,
  city: inputData.city,
  state: inputData.state,
});
const timestamp = String(Math.floor(Date.now() / 1000));
const requestId = crypto.randomUUID();
const signature = "sha256=" + crypto
  .createHmac("sha256", inputData.secret)
  .update(timestamp + "." + requestId + "." + body)
  .digest("hex");
output = { body, timestamp, requestId, signature };
  1. Action Webhooks by Zapier → Custom Request: Method POST, URL https://meetmeatthefair.com/api/v1/promoter/events, Data = the Code step's body (exactly as output, unchanged), Headers: Content-Type: application/json, X-MMATF-Key = your key ID, X-MMATF-Timestamp = timestamp, X-MMATF-Request-Id = requestId, X-MMATF-Signature = signature.
  2. Test it. A bad_signature almost always means the Data was changed after signing (Zapier reformatting, or Unflatten turned on): send the Code step's body as is.

Make recipe

  1. Add a Tools → Set multiple variables module with Secret (your secret), RequestId, Timestamp, Body and Signature, using Make's built-in uuid, formatDate and sha256 (with a key, sha256 computes the HMAC):
RequestId   {{uuid}}
Timestamp   {{formatDate(now; "X")}}
Body        {"external_id":"{{1.id}}","name":"{{1.name}}","start_date":"{{1.date}}","city":"{{1.city}}","state":"{{1.state}}"}
Signature   sha256={{sha256(Timestamp.RequestId.Body; hex; Secret)}}
            (in the editor: sha256( {{Timestamp}}.{{RequestId}}.{{Body}} ; hex ; {{Secret}} ),
             with a literal "." between the three variables)
  1. Add HTTP → Make a request: Method POST, URL https://meetmeatthefair.com/api/v1/promoter/events, Body type Raw, Content type JSON, Request content = the Body variable, and the four X-MMATF-* headers from the variables.
  2. Build Body once and use the same variable in both the signature and the request, so the signed bytes and the sent bytes are identical.

Coming later: a listed Zapier app

The recipes above use Zapier's and Make's generic HTTP steps. A listed "Meet Me at the Fair" app in Zapier's directory would remove the code step: it needs a Zapier developer-platform integration with an authentication scheme Zapier supports natively (an API key or OAuth in place of the request signing above), a "Create or update event" action and a "Cancel event" action mapped to these endpoints, a test account, and Zapier's review before it is public. Tell us if you would use it.