{"openapi":"3.1.0","info":{"title":"Ritmo API","version":"1.0.0","description":"The machine-to-machine surface of a Ritmo workspace. Every route here delegates to the same service the dashboard uses, so an API-created contact and a hand-typed one are the same row, and an API-sent message appears in the inbox and counts against the same meter.","contact":{"name":"Ritmo","url":"https://ritmo.uritm.com"}},"servers":[{"url":"https://ritmo-api.uritm.com/api","description":"Production"}],"security":[{"apiKey":[]}],"tags":[{"name":"Contacts","description":"People in the workspace, and saved audience filters."},{"name":"Messages","description":"Replying inside an existing conversation."},{"name":"OTP","description":"One-time passcodes and transactional SMS through the tenant’s own provider account."},{"name":"Templates","description":"Approved WhatsApp message templates."},{"name":"Broadcasts","description":"Bulk campaigns on WhatsApp, Telegram and SMS."},{"name":"Usage","description":"This calendar month, per channel."}],"paths":{"/v1/contacts":{"get":{"tags":["Contacts"],"operationId":"listContacts","summary":"List contacts","description":"Paged, newest first. `pageSize` is capped at 200 regardless of what you ask for.","security":[{"apiKey":["contacts:read"]}],"parameters":[{"name":"search","in":"query","required":false,"description":"Matches name, email or phone.","schema":{"type":"string","maxLength":200}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"A page of contacts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactPage"}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Contacts"],"operationId":"createContact","summary":"Create a contact","description":"At least one of `email` or `phone` is required: a contact with neither cannot be matched to a conversation, an import row or a broadcast recipient, so it is a row nothing can ever reach.","security":[{"apiKey":["contacts:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewContact"}}}},"responses":{"201":{"description":"The created contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"400":{"description":"Validation failed, or neither email nor phone was given (`CONTACT_NEEDS_IDENTITY`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/segments":{"get":{"tags":["Contacts"],"operationId":"listSegments","summary":"List saved audiences","description":"The filters a broadcast can target. `cachedCount` is for display only — a campaign re-runs the query when it starts, so a stale count never decides who gets messaged.","security":[{"apiKey":["contacts:read"]}],"responses":{"200":{"description":"Every segment in the workspace.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Segment"}}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/messages":{"post":{"tags":["Messages"],"operationId":"sendMessage","summary":"Reply in a conversation","description":"Sends into an existing conversation. There is no route to start one: on WhatsApp, Messenger and Instagram the customer has to write first, and a bulk first-contact belongs in a broadcast with an approved template. Outside a WhatsApp 24-hour window the message is stored as failed with code 131047 rather than being delivered.","security":[{"apiKey":["messages:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewMessage"}}}},"responses":{"201":{"description":"The queued message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageAccepted"}}}},"400":{"description":"Validation failed, or the conversation does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The monthly message allowance is spent or the workspace is suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanLimitError"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/otp/send":{"post":{"tags":["OTP"],"operationId":"sendOtp","summary":"Send an SMS","description":"Goes out on the workspace’s own SMS provider account, which must be verified with a test send first — an unverified provider is refused before any HTTP call, so a mistyped password cannot quietly burn your provider credit. Ritmo never stores the passcode.","security":[{"apiKey":["otp:send"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewOtp"}}}},"responses":{"201":{"description":"The gateway answered. Check `sent` — a rejected send is a 201 with `sent: false`, not an HTTP error, because the request itself was valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SmsResult"}}}},"400":{"description":"Validation failed, or no verified provider is configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"More than 60 sends in a minute for this workspace (`OTP_RATE_LIMITED`), or the credit balance is too low (`INSUFFICIENT_CREDITS`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanLimitError"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/templates":{"get":{"tags":["Templates"],"operationId":"listTemplates","summary":"List approved WhatsApp templates","description":"Only templates Meta has approved, because only those can be sent. `bodyVariableCount` counts the distinct `{{n}}` placeholders in the body, which is exactly how many parameters a send must supply.","security":[{"apiKey":["templates:read"]}],"responses":{"200":{"description":"Approved templates, by name.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Template"}}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/broadcasts":{"get":{"tags":["Broadcasts"],"operationId":"listBroadcasts","summary":"List campaigns","description":"The hundred most recent campaigns, newest first.","security":[{"apiKey":["broadcasts:read"]}],"responses":{"200":{"description":"Recent campaigns.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Broadcast"}}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/broadcasts/{id}/start":{"post":{"tags":["Broadcasts"],"operationId":"startBroadcast","summary":"Start or resume a campaign","description":"A draft or scheduled campaign moves to QUEUED and its audience is materialised on the next worker tick. A paused one goes straight back to RUNNING and keeps the recipients it already has.","security":[{"apiKey":["broadcasts:write"]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"The campaign, with its new status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Broadcast"}}}},"400":{"description":"The campaign is already running, completed or cancelled (`BROADCAST_NOT_STARTABLE`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No campaign with that id in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/usage":{"get":{"tags":["Usage"],"operationId":"getUsage","summary":"This month’s usage","description":"The calendar month in UTC, not a rolling thirty days. A `null` limit means unlimited on this plan.","security":[{"apiKey":["usage:read"]}],"responses":{"200":{"description":"Plan, limits and usage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummary"}}}},"401":{"description":"The key is missing, malformed, revoked or expired. `message` is one of MISSING_API_KEY, MALFORMED_API_KEY, INVALID_API_KEY.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key is valid but lacks the scope this route needs. `message` is MISSING_SCOPE:<scope>.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 120 requests in a minute for this key. The ceiling is per key, not per address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"publicKey:secret","x-tokenExample":"rk_live_XXXX:YOUR_SECRET","description":"Issue a key in Settings → API. Send both halves as one bearer token separated by a colon: `Authorization: Bearer rk_live_XXXX:YOUR_SECRET`. The public half is safe to log; the secret half is shown once, at creation, and can never be displayed again. A key with no scopes has full access — narrow it by granting only the scopes it needs."}},"schemas":{"Error":{"type":"object","properties":{"statusCode":{"type":"integer","example":401},"error":{"type":"string","example":"Unauthorized"},"message":{"type":"string","description":"A stable machine-readable code, not a sentence. Switch on this.","example":"INVALID_API_KEY"}},"required":["statusCode","message"]},"PlanLimitError":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"code":{"type":"string","enum":["MESSAGE_LIMIT_REACHED","SEAT_LIMIT_REACHED","BOT_LIMIT_REACHED","CHANNEL_LIMIT_REACHED","AI_REPLY_LIMIT_REACHED","SUBSCRIPTION_SUSPENDED","INSUFFICIENT_CREDITS","OTP_RATE_LIMITED"]},"limit":{"type":"integer"},"usage":{"type":"integer"}}}],"description":"A 402 carries `limit` and `usage` alongside the code, so a client can say \"500 of 500\" without a second round trip."},"Contact":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"notes":{"type":["string","null"]},"channelType":{"type":["string","null"],"enum":["WHATSAPP","MESSENGER","INSTAGRAM","TELEGRAM","TIKTOK","WEB",null]},"externalId":{"type":["string","null"],"description":"Phone number, Messenger PSID, or a stable web visitor id."},"tags":{"type":"array","items":{"type":"string"},"description":"Always lowercase. \"VIP\" and \"vip\" being two tags is a bug."},"attributes":{"type":"object","additionalProperties":true},"optedOut":{"type":"boolean"},"optedOutAt":{"type":["string","null"],"format":"date-time"},"lastSeenAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","tags","optedOut","createdAt"]},"NewContact":{"type":"object","description":"At least one of `email` or `phone` must be present.","properties":{"name":{"type":"string","maxLength":200},"email":{"type":"string","maxLength":200},"phone":{"type":"string","maxLength":40,"description":"E.164 without the plus, e.g. 201000000001."},"notes":{"type":"string","maxLength":5000},"tags":{"type":"array","items":{"type":"string"}},"attributes":{"type":"object","additionalProperties":true,"description":"Anything you want to address them by later. A campaign body can read these as {{attribute.city}}."}},"example":{"name":"Mona Ahmed","phone":"201000000001","tags":["vip"],"attributes":{"city":"Cairo"}}},"ContactPage":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}}},"required":["total","page","pageSize","items"]},"Segment":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"definition":{"type":"object","description":"A match mode and a rule list. An empty rule list means everyone who has not opted out, never nobody.","properties":{"match":{"type":"string","enum":["all","any"]},"rules":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"cachedCount":{"type":["integer","null"]},"cachedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}},"required":["id","name","definition"]},"NewMessage":{"type":"object","properties":{"conversationId":{"type":"string","minLength":1},"text":{"type":"string","minLength":1,"maxLength":4096,"description":"4096 is the Telegram ceiling, the lowest of the channels involved."}},"required":["conversationId","text"],"example":{"conversationId":"clx…","text":"Your order shipped today."}},"MessageAccepted":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","description":"The status at the moment of the reply. Dispatch settles afterwards, so a `sent` here can still become `failed` — subscribe to the message.status webhook rather than polling.","example":"sent"}},"required":["id","status"]},"NewOtp":{"type":"object","properties":{"mobile":{"type":"string","minLength":6,"maxLength":20,"example":"201000000001"},"message":{"type":"string","minLength":1,"maxLength":1000},"lang":{"type":"string","enum":["E","A"],"description":"Message encoding: E for Latin, A for Arabic. Defaults to E."}},"required":["mobile","message"],"example":{"mobile":"201000000001","message":"Your code is 448120","lang":"E"}},"SmsResult":{"type":"object","properties":{"requestId":{"type":"string","description":"Ritmo’s record, for the OTP log."},"sent":{"type":"boolean"},"provider":{"type":"string","example":"smsmisr"},"error":{"type":"string","description":"Present only when `sent` is false."},"errorCode":{"type":"string","description":"Present only when `sent` is false. The code, not just the text, so you can tell \"this number is unreachable, ever\" from \"the gateway was briefly down\"."}},"required":["requestId","sent","provider"]},"Template":{"type":"object","properties":{"id":{"type":"string"},"metaId":{"type":"string"},"name":{"type":"string"},"language":{"type":"string","example":"ar"},"category":{"type":"string","example":"MARKETING"},"status":{"type":"string","example":"APPROVED"},"components":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Meta’s own component array, stored verbatim."},"bodyVariableCount":{"type":"integer"},"headerFormat":{"type":["string","null"]},"lastSyncedAt":{"type":"string","format":"date-time"}},"required":["id","name","language","status","bodyVariableCount"]},"Broadcast":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"channelId":{"type":"string"},"channelType":{"type":"string","enum":["WHATSAPP","TELEGRAM"]},"kind":{"type":"string","enum":["WHATSAPP_TEMPLATE","TEXT","SMS"],"description":"Messenger and Instagram are absent on purpose: Meta’s message-tag policy makes bulk marketing there a violation."},"status":{"type":"string","enum":["DRAFT","SCHEDULED","QUEUED","RUNNING","PAUSED","COMPLETED","CANCELLED","FAILED"]},"templateId":{"type":["string","null"]},"body":{"type":["string","null"],"description":"TEXT and SMS only. May address each recipient with {{name}}, {{first_name}}, {{phone}}, {{email}} or {{attribute.<key>}}. An unknown placeholder renders empty."},"scheduledAt":{"type":["string","null"],"format":"date-time"},"rateLimitPerMinute":{"type":"integer","description":"Halved automatically on a provider 429, floor 10."},"totalCount":{"type":"integer"},"sentCount":{"type":"integer"},"deliveredCount":{"type":"integer"},"readCount":{"type":"integer"},"failedCount":{"type":"integer"},"skippedCount":{"type":"integer","description":"Opted-out and address-less contacts. Recorded rather than omitted, so the report accounts for the whole audience."},"createdAt":{"type":"string","format":"date-time"}},"required":["id","name","kind","status","channelType"]},"ChannelUsage":{"type":"object","properties":{"channelId":{"type":["string","null"]},"channelType":{"type":["string","null"]},"displayName":{"type":["string","null"]},"sent":{"type":"integer"},"received":{"type":"integer"},"failed":{"type":"integer","description":"Rejected outbound. It still spent a quota slot."},"aiReplies":{"type":"integer"}},"required":["sent","received","failed","aiReplies"]},"UsageSummary":{"type":"object","properties":{"plan":{"type":"string","enum":["free","starter","growth","business"]},"limits":{"type":"object","description":"A null value means unlimited on this plan.","properties":{"messages":{"type":["integer","null"]},"seats":{"type":["integer","null"]},"bots":{"type":["integer","null"]},"channels":{"type":["integer","null"]},"aiReplies":{"type":["integer","null"]}}},"usage":{"type":"object","properties":{"messages":{"type":"integer"},"seats":{"type":"integer"},"bots":{"type":"integer"},"channels":{"type":"integer"},"aiReplies":{"type":"integer"},"failed":{"type":"integer"},"byChannel":{"type":"array","items":{"$ref":"#/components/schemas/ChannelUsage"}}}},"period":{"type":"object","description":"The same month, broken out per channel.","properties":{"total":{"type":"integer"},"sent":{"type":"integer"},"received":{"type":"integer"},"failed":{"type":"integer"},"aiReplies":{"type":"integer"},"byChannel":{"type":"array","items":{"$ref":"#/components/schemas/ChannelUsage"}}}},"periodStart":{"type":"string","format":"date-time"},"periodEnd":{"type":"string","format":"date-time"},"percentUsed":{"type":["integer","null"]}},"required":["plan","limits","usage","periodStart","periodEnd"]}}}}