Developers
Connect your own code to Marketiism OS.
Use the REST API to read and write contacts, deals and bookings and to read orders, products and invoices. Webhooks send a signed HTTPS request to your server when something happens in your workspace, such as a paid order or a new booking. Both are on the Growth and Agency plans.
Authentication
Every request carries a workspace API key as a Bearer token. The key decides the workspace; you never pass a workspace id. Keys look like mos_<prefix>_<secret>.
- Sign in and open Settings, then API keys. Owners and admins can create keys.
- Give the key a name and choose its scopes. The full key is shown once, so copy it then.
- Send it on every request in the Authorization header, over HTTPS only.
- Revoke a key from the same page if it leaks. Requests with it fail straight away.
Base URL
https://marketiism.online/api/v1/Example request
curl https://marketiism.online/api/v1/contacts/?limit=20 \
-H "Authorization: Bearer mos_xxxx_your_secret"Scopes
Each key can be limited to the resources it needs. A write scope includes read. A key with no scopes set uses its read or read-write access for every resource. A missing scope returns 403.
| Resource | Read scope | Write scope |
|---|---|---|
| Contacts | contacts:read | contacts:write |
| Tags | tags:read | Read only |
| Deals | deals:read | deals:write |
| Orders | orders:read | Read only |
| Products | products:read | Read only |
| Bookings | bookings:read | bookings:write |
| Invoices | invoices:read | Read only |
Pagination
Lists use cursors. Pass limit (1 to 100, default 50) and follow the next link until has_more is false. Don't build cursors yourself; they are opaque.
{
"data": [ { "id": 812, "name": "Pooja Kulkarni", ... } ],
"next": "https://marketiism.online/api/v1/contacts/?cursor=cD0yMDI2...",
"previous": null,
"has_more": true
}Errors
Errors share one JSON shape. Use the code field in your logic; the message is for people.
{
"error": {
"code": "permission_denied",
"message": "Missing scope deals:write.",
"details": null
}
}| Code | HTTP | When |
|---|---|---|
| invalid | 400 | A field is missing or wrong. details names the fields. |
| not_authenticated | 401 | No Authorization header. |
| authentication_failed | 401 | The key is wrong or revoked. |
| plan_limit_reached | 402 | Your plan limit is reached, for example the contact limit. |
| permission_denied | 403 | The key doesn't have the scope for this call. |
| not_found | 404 | No such record in this workspace. |
| method_not_allowed | 405 | That method isn't supported on this path. |
| booking_unavailable | 409 | The booking can't be made, for example the slot was just taken. |
| throttled | 429 | Too many requests. Wait for the seconds in Retry-After. |
Rate limits
Each key can make 120 requests every 60 seconds. Every response has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. Over the limit you get 429 with Retry-After. Too many wrong keys from one IP address in a minute also get 429.
Resources and endpoints
Paths are relative to the base URL. Money is in paise (integer), times are ISO 8601 with time zone.
The full OpenAPI 3.1 document is at https://marketiism.online/api/v1/openapi.json. Import it into Postman or generate a client from it.
Contacts
| Method | Path | What it does |
|---|---|---|
| GET | /contacts/ | List contacts |
| POST | /contacts/ | Create |
| GET | /contacts/{id}/ | Get one |
| PATCH | /contacts/{id}/ | Update |
| DELETE | /contacts/{id}/ | Delete |
| POST | /contacts/{id}/tags/ | Add a tag |
| DELETE | /contacts/{id}/tags/{tag}/ | Remove a tag |
Deals
| Method | Path | What it does |
|---|---|---|
| GET | /deals/ | List deals |
| POST | /deals/ | Create |
| GET | /deals/{id}/ | Get one |
| PATCH | /deals/{id}/ | Update |
| DELETE | /deals/{id}/ | Delete |
Pipelines
| Method | Path | What it does |
|---|---|---|
| GET | /pipelines/ | List pipelines |
| GET | /pipelines/{id}/ | Get one |
Products
| Method | Path | What it does |
|---|---|---|
| GET | /products/ | List products |
| GET | /products/{id}/ | Get one |
Orders
| Method | Path | What it does |
|---|---|---|
| GET | /orders/ | List orders |
| GET | /orders/{id}/ | Get one |
Booking services
| Method | Path | What it does |
|---|---|---|
| GET | /booking-services/ | List booking-services |
| GET | /booking-services/{id}/ | Get one |
Bookings
| Method | Path | What it does |
|---|---|---|
| GET | /bookings/ | List bookings |
| POST | /bookings/ | Create |
| GET | /bookings/{id}/ | Get one |
| POST | /bookings/{id}/cancel/ | Cancel a booking |
Invoices
| Method | Path | What it does |
|---|---|---|
| GET | /invoices/ | List invoices |
| GET | /invoices/{id}/ | Get one |
Tags
| Method | Path | What it does |
|---|---|---|
| GET | /tags/ | List tags in use |
Webhooks
Add an endpoint in the console under Settings, then Webhooks: an https URL on port 443 and the events you want. The signing secret is shown once when you create the endpoint or rotate it. You can send a test ping and see every delivery with its response code.
Every delivery is a POST with a JSON body like this. data has the same shape as the API.
{
"id": "6f1c2b9e-4d1a-4b8e-9a51-0c7d2f3e8a10",
"event": "booking.created",
"created": 1791100800,
"workspace": "sharma-dental",
"data": { "id": 3141, "status": "confirmed", ... }
}Headers
| Header | What it holds |
|---|---|
| X-Marketiism-Signature | t=<unix time>,v1=<hex HMAC-SHA256>. See the steps below. |
| X-Marketiism-Event | The event name, for example order.paid. |
| X-Marketiism-Delivery | Unique id of this event (the id in the body). Store it to skip duplicates. |
Events (22)
| Event | Sent when |
|---|---|
| contact.created | A contact was created (form, import, API, console). |
| contact.tag_added | A tag was added to a contact. |
| contact.tag_removed | A tag was removed from a contact. |
| form.submitted | An opt-in form was submitted. |
| deal.created | A deal was created. |
| deal.stage_changed | A deal moved to another stage. |
| order.paid | An order was paid (confirmed by the payment gateway webhook). |
| booking.created | A booking was made. |
| booking.confirmed | A prepaid booking was confirmed after payment. |
| booking.cancelled | A booking was cancelled. |
| booking.rescheduled | A booking moved to a new time (data.old_start). |
| booking.completed | A booking was marked completed. |
| booking.no_show | A booking was marked no-show. |
| subscription.created | A subscription was created. |
| subscription.activated | A subscription became active. |
| subscription.cancelled | A subscription was cancelled. |
| subscription.paused | A subscription was paused. |
| subscription.resumed | A subscription was resumed. |
| subscription.past_due | A renewal payment failed (dunning started). |
| subscription.charged | A renewal was charged (data.order). |
| invoice.created | A tax invoice was issued (after an order is paid). |
| credit_note.created | A credit note was issued (refund). |
Verify the signature
- Read the raw request body as bytes, before any JSON parsing.
- Split the X-Marketiism-Signature header on commas into t and v1.
- Reject the request if t is more than 5 minutes away from your clock.
- Compute HMAC-SHA256 with your endpoint secret over t, a full stop, and the raw body.
- Compare the hex result with v1 using a constant-time comparison. Reject on mismatch.
- Reply with any 2xx within 5 seconds, then do the slow work in the background.
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
try:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts["t"])
except (ValueError, KeyError):
return False
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
# Django: verify(SECRET, request.headers.get("X-Marketiism-Signature", ""), request.body)const crypto = require("crypto");
function verify(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries((header || "").split(",").map((p) => p.split("=", 2)));
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
const given = Buffer.from(parts.v1 || "");
return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected));
}
// Express: use express.raw({ type: "application/json" }) on this route so req.body is the raw Buffer.Retries and auto-disable
A delivery that doesn't get a 2xx in 5 seconds is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, so 6 attempts in all. Redirects are not followed. After 10 failed attempts in a row the endpoint is turned off and workspace owners are notified. Fix it and turn it back on from the console. Deliveries can arrive more than once or out of order, so use the delivery id and the created time.