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.
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_idcreates 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_idstages your changes for review:outcome: "update_staged"withchanges, 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), orskipwith 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.
| Field | Required to create | Example |
|---|---|---|
name | Yes | Fall Craft Fair |
start_date | Yes | 2027-10-02 |
end_date | 2027-10-03 | |
daily_hours | 9am-4pm | |
venue_name | Town Hall | |
address | 12 Main St | |
city | Yes | Concord |
state | Yes | NH |
zip | 03301 | |
description | 60 juried artisans, food and music. | |
categories | Craft Fair; Holiday Market | |
admission | Free | |
vendor_application_url | https://example.org/apply | |
application_deadline | 2027-08-15 | |
booth_fee_min | 75 | |
booth_fee_max | 125 | |
event_url | https://example.org/fall-fair | |
image_url | https://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": "…"}].
| HTTP | code | Meaning |
|---|---|---|
| 401 | missing_auth_headers | A signing header is missing or malformed. |
| 401 | invalid_key | The key is unknown or revoked. |
| 401 | stale_timestamp | X-MMATF-Timestamp is more than 300 seconds from our clock. Check the sender's clock. |
| 401 | bad_signature | The signature does not match. See the signing recipe. |
| 409 | replayed_request | This request id was already used with this key. Send a new one. |
| 409 | conflict_other_promoter | The event matches one another organizer lists. Email us if it is yours. |
| 409 | conflict_external_id | The event matches one of yours already linked to a different external_id. |
| 409 | in_progress | Another request for this external_id is still running. Retry shortly. |
| 404 | not_found | No event with that external_id for your key. |
| 413 | body_too_large | The body is over 64 KB. |
| 400 | invalid_json | The body is not JSON. |
| 422 | validation_failed | error.errors lists each problem as {field, message}. |
| 429 | rate_limited | More than 60 requests a minute for this key. Wait for Retry-After. |
| 500 | internal_error | Our side failed. Retry with a new request id. |
Zapier recipe
- Trigger: wherever your events live (a Google Sheet row, a form, your calendar).
- 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 };- Action Webhooks by Zapier → Custom Request: Method POST, URL
https://meetmeatthefair.com/api/v1/promoter/events, Data = the Code step'sbody(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. - Test it. A
bad_signaturealmost always means the Data was changed after signing (Zapier reformatting, or Unflatten turned on): send the Code step's body as is.
Make recipe
- Add a Tools → Set multiple variables module with Secret (your secret), RequestId, Timestamp, Body and Signature, using Make's built-in
uuid,formatDateandsha256(with a key,sha256computes 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)- 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 fourX-MMATF-*headers from the variables. - 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.