RCS-Box API
Multi-Tenant API Gateway for Google RCS Business Messaging. Send text messages via your brand's agents, list your agents, and receive user-message callbacks — all through a single, authenticated API.
Multi-Tenant
Brand-level isolation with dedicated API keys per tenant.
Tenant allow-list
Each tenant has a configurable country allow-list (default: +49). The send endpoint rejects numbers outside it.
Opt-out Management
Built-in STOP/START blacklist handling per agent.
Base URL
All API requests are made to the following base URL:
https://api.box.rcs.jetzt
All endpoints use JSON for request and response bodies.
Authentication
All /v1/* endpoints require an API key.
Each tenant receives a unique 64-character hex key. You can authenticate using either method:
Option 1: Authorization Header (recommended)
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/agents
Option 2: X-API-Key Header
curl -H "X-API-Key: $API_KEY" \
https://api.box.rcs.jetzt/v1/agents
Important: Keep your API key secret. Do not expose it in client-side code or public repositories. If compromised, contact your administrator to regenerate the key.
Endpoints
Health
/health
No auth required
Public health check endpoint. Use this to verify the API is running.
Response
{
"status": "ok",
"timestamp": "2026-03-26T12:00:00Z"
}
/v1/health
Auth required
Authenticated health check. Verifies both API availability and API key validity.
Response
{
"status": "ok",
"version": "v1",
"timestamp": "2026-03-26T12:00:00Z"
}
Agents
/v1/agents
Auth required
Returns all RCS agents belonging to your tenant's brand. Only agents associated with your brand are visible.
Example Request
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/agents
Response
{
"agents": [
{
"id": 1,
"tenant_id": 1,
"google_agent_id": "brands/acme-corp/agents/my-agent",
"brand_id": "acme-corp",
"name": "ACME Support Bot",
"display_name": "ACME Support Bot",
"description": "ACME customer support agent for order questions.",
"status": "LAUNCHED",
"hosting_region": "EUROPE",
"billing_category": "CONVERSATIONAL",
"use_case": "PROMOTIONAL",
"synced_at": "2026-03-25T10:30:00Z",
"created_at": "2026-01-20T14:00:00Z",
"updated_at": "2026-03-25T10:30:00Z"
}
]
}
Agent Statuses
| Status | Description |
|---|---|
DRAFT | Agent created but not yet submitted for launch |
LAUNCHING | Agent is under review by Google |
LAUNCHED | Agent is live and can send messages |
/v1/agents/:id
Auth required
Returns details for a specific agent. The agent must belong to your tenant.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | integer | The agent's internal ID |
Example Request
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://rcs-box.example.com/v1/agents/1
Response
{
"agent": {
"id": 1,
"tenant_id": 1,
"google_agent_id": "brands/acme-corp/agents/my-agent",
"brand_id": "acme-corp",
"name": "ACME Support Bot",
"display_name": "ACME Support Bot",
"description": "ACME customer support agent for order questions.",
"status": "LAUNCHED",
"hosting_region": "EUROPE",
"billing_category": "CONVERSATIONAL",
"use_case": "PROMOTIONAL",
"synced_at": "2026-03-25T10:30:00Z",
"created_at": "2026-01-20T14:00:00Z",
"updated_at": "2026-03-25T10:30:00Z"
}
}
Error Response (404)
{
"error": "Not Found",
"message": "Agent not found"
}
/v1/agents
Auth required
Create a new RCS agent under your tenant's brand. The agent starts in DRAFT
status and must be launched by an administrator before it can send messages.
Image URIs must be reachable by Google
When you call this endpoint, Google fetches logo_uri and
hero_uri server-side and stores the binary in its own CDN.
The URLs must be publicly reachable HTTPS at request time — localhost,
private networks and unresolved hosts produce a Google URL_ERROR /
INVALID_ARGUMENT and the agent is not created.
End-user devices cache these images aggressively — once a phone has displayed
an agent's logo/hero, updates via
PATCH (updateMask=logoUri)
may not be reflected until the cache expires or the app data is cleared. Upload your
production-quality assets before launching; don't rely on being able to swap a placeholder later.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
display_name | string | Yes | Human-readable name shown to end users (max. 40 chars) |
description | string | Yes | Short description of the agent (max. 100 chars) |
logo_uri | string | Yes | Publicly reachable HTTPS URL — PNG/JPG, 224×224 px, max. 50 KB |
hero_uri | string | Yes | Publicly reachable HTTPS URL — PNG/JPG, 1440×448 px, max. 200 KB |
color | string | Yes | Brand color as hex (e.g. #1DA1F2) |
privacy_uri | string | Yes | HTTPS URL to your privacy policy |
privacy_label | string | No | Display label for the privacy link (default: Datenschutz) |
terms_uri | string | Yes | HTTPS URL to your terms & conditions / Impressum |
terms_label | string | No | Display label for the terms link (default: Impressum) |
phones | array | No* | Up to 3 entries of {"number", "label"} — number in E.164 format (e.g. +4922199999701), label defaults to Telefon |
emails | array | No* | Up to 3 entries of {"address", "label"} — label defaults to E-Mail |
websites | array | No* | Up to 3 entries of {"uri", "label"} — HTTPS only, label defaults to Webseite |
website_uri | string | No* | Legacy single-website shorthand — HTTPS only, counts as the first websites entry |
website_label | string | No | Display label for website_uri (default: Webseite) |
hosting_region | string | Yes | One of EUROPE, NORTH_AMERICA, ASIA_PACIFIC |
billing_category | string | Yes | One of CONVERSATIONAL, NON_CONVERSATIONAL, BASIC_MESSAGE, SINGLE_MESSAGE, CONVERSATIONAL_MESSAGE, BASIC_MESSAGE_V2 |
use_case | string | No | One of TRANSACTIONAL, PROMOTIONAL, OTP, MULTI_USE (default: PROMOTIONAL) |
* At least one contact method is required
Google shows the agent's contact details on its info sheet and requires at least one.
Provide any combination of phones,
emails and websites
(max. 3 entries each) — or the legacy website_uri.
Requests without any contact are rejected with {"errors": {"contacts": "missing"}}.
Example Request
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "ACME Support Bot",
"description": "ACME customer support agent for order questions.",
"logo_uri": "https://acme.example.com/logo-224.png",
"hero_uri": "https://acme.example.com/hero-1440x448.png",
"color": "#1DA1F2",
"privacy_uri": "https://acme.example.com/privacy",
"terms_uri": "https://acme.example.com/terms",
"phones": [{"number": "+4922199999701", "label": "Hotline"}],
"emails": [{"address": "support@acme.example.com"}],
"websites": [{"uri": "https://acme.example.com", "label": "Webseite"}],
"hosting_region": "EUROPE",
"billing_category": "CONVERSATIONAL",
"use_case": "PROMOTIONAL"
}' \
https://api.box.rcs.jetzt/v1/agents
Smoke testing without real brand assets
For pure end-to-end smoke tests, dummyimage.com returns a real PNG at the exact dimensions and is publicly reachable by Google. Use it only for throwaway test agents — do not ship to production with placeholder images, because end-user phones will cache them.
"logo_uri": "https://dummyimage.com/224x224/1da1f2/ffffff.png",
"hero_uri": "https://dummyimage.com/1440x448/1da1f2/ffffff.png"
Error Response (422 — local validation)
When required fields are missing or invalid, you get a field-keyed hash of stable error codes before any call to Google. Codes are language-neutral so you can map them to your own UX:
{
"error": "Validation failed",
"errors": {
"description": "missing",
"hero_uri": "missing",
"color": "invalid_format"
}
}
Possible codes: missing, too_long, invalid_format, invalid_value, too_many (contact lists over 3 entries). A request without any contact method fails with {"errors": {"contacts": "missing"}}.
Error Response (422 — Google rejected payload)
If local validation passes but Google rejects the request (e.g. an image URL it can't fetch, a permission problem, or a constraint it enforces server-side), the upstream message is forwarded:
{
"error": "Google API Error",
"message": "Unexpected response: 400 - The supplied file URL https://example.com/logo.png is invalid, status: URL_ERROR"
}
Response (201)
{
"agent": {
"id": 7,
"tenant_id": 1,
"google_agent_id": "brands/acme-corp/agents/support-bot-xyz",
"brand_id": "acme-corp",
"name": "ACME Support Bot",
"display_name": "ACME Support Bot",
"description": "ACME customer support agent for order questions.",
"status": "DRAFT",
"hosting_region": "EUROPE",
"billing_category": "CONVERSATIONAL",
"use_case": "PROMOTIONAL",
"synced_at": "2026-04-20T09:00:00Z",
"created_at": "2026-04-20T09:00:00Z",
"updated_at": "2026-04-20T09:00:00Z"
}
}
Messages
/v1/agents/:agent_id/messages
Auth required
Send any RCS message type via a specific agent — text, rich card, carousel, file, or text with interactive suggestions. The agent must belong to your tenant.
Pre-launch sends are restricted to accepted testers
If the agent is in
LAUNCHED status, it can send to any recipient (subject to the
tenant's country allow-list and per-agent blacklist). If the agent is still in
DRAFT or any other pre-launch status, the recipient phone
must be registered as a tester on that agent and have accepted the invitation
(tester status = "ACCEPTED"). Otherwise the request returns
422 Agent not launched.
This mirrors Google's RCS tester workflow: register testers via
POST /v1/agents/:id/testers, the user receives an
SMS invite from Google, and once they accept the invite the tester's
status flips to ACCEPTED.
Phone matching is done by normalized digits — spaces and dashes are ignored.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | Recipient phone number in E.164 format. Must match one of the tenant's configured country prefixes (default: +49). |
content_message | object | Yes | The Google RCS contentMessage payload. Must contain exactly one of text, richCard, contentInfo, fileName, or uploadedRbmFile, optionally plus suggestions (max 11). |
The content_message shape mirrors
Google's RCS Business Messaging AgentContentMessage
— see the sub-sections below for each supported type.
Response (201)
{
"message": {
"id": 42,
"tenant_id": 3,
"agent_id": 7,
"google_message_id": "phones/+491701234567/agentMessages/msg-abc-123",
"phone": "+491701234567",
"status": "SENT",
"message_type": "text",
"content": { "text": "Hello!" },
"direction": "outbound",
"sent_at": "2026-04-24T12:00:00Z",
"delivered_at": null,
"error_message": null,
"created_at": "2026-04-24T12:00:00Z",
"updated_at": "2026-04-24T12:00:00Z"
}
}
message_type is one of
text, rich_card,
carousel, file.
Text message
The simplest message type. Max 3072 characters.
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491701234567",
"content_message": {
"text": "Hallo! Willkommen bei ACME."
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages
Text with suggestions
Suggestions are interactive chips shown below the message. You can mix any of the four action types per message — up to 11 suggestions total.
Suggestion types
reply— tapping sends a reply back (you receivepostbackDatavia webhook)action.openUrlAction— opens an external URLaction.dialAction— starts a phone callaction.viewLocationAction— opens a location on a map
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491701234567",
"content_message": {
"text": "Wie können wir helfen?",
"suggestions": [
{
"reply": {
"text": "Bestellung verfolgen",
"postbackData": "track_order"
}
},
{
"action": {
"text": "Website öffnen",
"postbackData": "open_site",
"openUrlAction": { "url": "https://acme.example.com" }
}
},
{
"action": {
"text": "Hotline anrufen",
"postbackData": "call_hotline",
"dialAction": { "phoneNumber": "+498000000000" }
}
},
{
"action": {
"text": "Filiale auf Karte",
"postbackData": "show_store",
"viewLocationAction": {
"latLong": { "latitude": 50.9413, "longitude": 6.9583 },
"label": "ACME Köln"
}
}
}
]
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages
Standalone Rich Card
A card with title, description, media and up to 4 actions. Use
cardOrientation: VERTICAL for
full-width media at the top, HORIZONTAL for side-by-side.
Media height: SHORT, MEDIUM, or TALL.
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491701234567",
"content_message": {
"richCard": {
"standaloneCard": {
"cardOrientation": "VERTICAL",
"cardContent": {
"title": "Neuer Hoodie 🔥",
"description": "Limited Edition — jetzt im Shop.",
"media": {
"height": "MEDIUM",
"contentInfo": {
"fileUrl": "https://cdn.example.com/hoodie.jpg",
"thumbnailUrl": "https://cdn.example.com/hoodie-thumb.jpg"
}
},
"suggestions": [
{
"action": {
"text": "Jetzt kaufen",
"postbackData": "buy_hoodie",
"openUrlAction": { "url": "https://shop.example.com/hoodie" }
}
},
{
"reply": { "text": "Mehr Infos", "postbackData": "info_hoodie" }
}
]
}
}
}
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages
Carousel
Between 2 and 10 cards arranged horizontally. Users swipe through them.
cardWidth: SMALL or MEDIUM.
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491701234567",
"content_message": {
"richCard": {
"carouselCard": {
"cardWidth": "MEDIUM",
"cardContents": [
{
"title": "Produkt A",
"description": "19,99 €",
"media": {
"height": "SHORT",
"contentInfo": { "fileUrl": "https://cdn.example.com/a.jpg" }
},
"suggestions": [
{
"action": {
"text": "Ansehen",
"postbackData": "view_a",
"openUrlAction": { "url": "https://shop.example.com/a" }
}
}
]
},
{
"title": "Produkt B",
"description": "24,99 €",
"media": {
"height": "SHORT",
"contentInfo": { "fileUrl": "https://cdn.example.com/b.jpg" }
},
"suggestions": [
{
"action": {
"text": "Ansehen",
"postbackData": "view_b",
"openUrlAction": { "url": "https://shop.example.com/b" }
}
}
]
}
]
}
}
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages
File / Media
Send a file (image, PDF, video, audio) either by URL or by referencing a file previously uploaded to Google's RBM file store.
By URL
Use contentInfo on its own — it is its own member of
Google's contentMessage oneof, not a companion to
fileName. Combining them returns Google
400 INVALID_ARGUMENT (“oneof field 'content' is already set”).
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491701234567",
"content_message": {
"contentInfo": {
"fileUrl": "https://cdn.example.com/rechnungen/12345.pdf",
"thumbnailUrl": "https://cdn.example.com/rechnungen/12345-thumb.jpg"
}
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages
Via uploaded RBM file
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491701234567",
"content_message": {
"uploadedRbmFile": {
"fileName": "files/abc123",
"thumbnailName": "files/abc123-thumb"
}
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages
Common errors
Invalid content_message (422)
{
"error": "Invalid content_message",
"errors": { "text": "too_long" }
}
Stable error codes per field:
missing— required field absent or emptytoo_long— string exceeds max length (text: 3072)too_many— array exceeds limit (suggestions: 11, card suggestions: 4)invalid_value— value not in the allowed enum (e.g. cardOrientation)invalid_size— carousel needs 2–10 cardsinvalid_structure— wrong shape for the given typemissing_primary— need one oftext,richCard,contentInfo,fileName,uploadedRbmFilemultiple_primary— only one primary key allowed per messageunknown_keys—content_messagecontains unsupported fields
Phone not in tenant allow-list (400)
The Allowed prefixes list reflects your tenant's configured allowed_countries at request time. Example for a tenant configured with Germany and Austria:
{
"error": "Bad Request",
"message": "Phone number is not in the tenant's allow-list. Allowed prefixes: +49, +43"
}
Agent not launched (422)
Returned when the agent is pre-launch and the recipient is not an accepted tester. Register the number as a tester and have them accept the invite before retrying.
{
"error": "Agent not launched",
"message": "Agent '…' is in status DRAFT. Before launch, only accepted testers may receive messages — register +491701234567 as a tester and have them accept the invitation."
}
Phone blacklisted (403)
Returned when the recipient is on this agent's blacklist. Two distinct cases share this response: (1) the user sent a STOP/opt-out keyword — clears once they send START/opt-in; and (2) an operator added the number manually via the admin blacklist — clears only when an operator removes it. The error body is identical in both cases.
{
"error": "Forbidden",
"message": "Phone number has opted out for this agent"
}
Note: STOP/START handling is managed internally — when a user replies "STOP" (or
German equivalents) the phone number is added to the agent's blacklist. The original payload is still
forwarded to your webhook_url, but enriched with an rcs_box_action
field (OPT_OUT / OPT_IN) so you can mirror the
state in your own system — see the Opt-out / Opt-in section.
The send endpoint rejects blacklisted numbers with 403 Forbidden.
Quota exceeded (402)
Returned when the tenant's plan has a hard cap and the next send would exceed it (today: the
free plan). Paid plans never return 402; they accept the send and accrue overage
usage. The error_key field is the machine-readable reason — branch on it
rather than parsing message.
{
"error": "Quota exceeded",
"error_key": "quota_exceeded_free_tier",
"message": "Quota exceeded for plan 'free': 100/100 session(s) this period"
}
Note: Pre-launch sends to accepted testers do not count toward the quota
— only sends made while the agent is LAUNCHED are billable.
Quotas reset at the start of each calendar month.
/v1/agents/:agent_id/messages/video
Convenience wrapper: sends a URL inside a plain text message so the RCS client auto-unfurls it
into a native preview. Works for YouTube, Vimeo and direct video links — the client renders a
tappable thumbnail without you having to upload or host the media yourself. An optional
caption is prepended on its own two lines.
Request Body
| Field | Type | Description |
|---|---|---|
phone | string | Recipient phone in E.164 format (e.g. +491791234567). |
video_url | string | Any URL the RCS client will unfurl (YouTube, Vimeo, direct MP4, …). |
caption | string, optional | Text shown above the preview. Blank = URL only. |
Example
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491791234567",
"video_url": "https://youtu.be/kg9bFxsJXlE",
"caption": "🎥 RCS in 90 Sekunden"
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages/video
Internally this sends a plain {"text": "caption\n\nurl"} contentMessage.
The response shape is identical to Send Message.
/v1/agents/:agent_id/messages/image
Convenience wrapper that sends a standalone image by URL — the RCS client downloads and renders
it inline. Accepts an optional thumbnail_url for a lighter preview
before the full image loads.
Request Body
| Field | Type | Description |
|---|---|---|
phone | string | Recipient phone in E.164 format. |
image_url | string | Publicly reachable HTTPS URL to the full-size image. |
thumbnail_url | string, optional | Lightweight preview loaded first. |
Example
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491791234567",
"image_url": "https://dummyimage.com/1080x720/1DA1F2/ffffff.png&text=Hallo",
"thumbnail_url": "https://dummyimage.com/270x180/1DA1F2/ffffff.png&text=Hallo"
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages/image
Internally this sends a {"contentInfo": {"fileUrl": "…", "thumbnailUrl": "…"}}
contentMessage. The response shape is identical to Send Message.
/v1/agents/:agent_id/messages/xl
Sends 2–3 content messages in sequence to the same phone. Maps to Telekom's XL Message billing concept: up to three messages delivered within 60 seconds are consolidated into a single billing event, so you can split a richer payload (e.g. text + image + text) across multiple bubbles without paying three times.
Fail-fast with partial success
All messages are validated up-front. If one is invalid, nothing is sent and a
422 is returned with failed_at pointing
at the offending index. If Google rejects a message mid-sequence, the endpoint returns
422 with the successfully-sent logs in messages
plus failed_at and the error on the first failure. Already-sent messages
remain delivered — there is no rollback.
Request Body
| Field | Type | Description |
|---|---|---|
phone | string | Recipient phone in E.164 format. |
messages | array (2–3) | Each item is a full content_message object — text, richCard, contentInfo etc. |
delay_seconds | number, optional | Pause between sends so the handset receives them in order. Defaults to 2.0, clamped to [0, 10]. Google delivers sequential sends async to the handset — without a buffer, messages can arrive out of order. |
Example
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491791234567",
"messages": [
{ "text": "🎉 Unsere neue Kollektion ist da!" },
{ "contentInfo": { "fileUrl": "https://dummyimage.com/1080x720" } },
{ "text": "Jetzt im Shop: https://example.com/shop" }
]
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages/xl
Success (201)
{
"messages": [
{ "id": 101, "google_message_id": "…/msg-1", "message_type": "text", … },
{ "id": 102, "google_message_id": "…/msg-2", "message_type": "file", … },
{ "id": 103, "google_message_id": "…/msg-3", "message_type": "text", … }
]
}
Partial success — Google rejects item #1 (422)
{
"messages": [ { "id": 101, …, "message_type": "text" } ],
"failed_at": 1,
"error": "Google API Error",
"message": "…"
}
/v1/agents/:agent_id/messages/final
Single-use "goodbye" send that bypasses the per-agent blacklist for ONE message immediately
after a user opts out. Authorized via the consent_audit_log_id that
RCS-Box passed in the OPT_OUT forward — see the
Opt-out / Opt-in section. Use it for the polite
"Tschüss + STOP-Hinweis-Wiederholung"-Pattern; every subsequent send to that phone still hits
the normal 403 Forbidden.
Tightly scoped exception, not a removal
The DSGVO fail-safe stays intact: the phone is blacklisted synchronously when the user sent STOP,
and that doesn't change. The bypass is consumed by the first call within
5 minutes after the tenant acked the OPT_OUT and is then dead. A second call
returns 403.
Request Body
| Field | Type | Description |
|---|---|---|
phone | string | E.164 phone of the user who opted out. Must match the audit log's normalized phone. |
consent_audit_log_id | integer | The ID RCS-Box added to the OPT_OUT forward payload. Must belong to your tenant, target this agent and phone, be acked, and not yet consumed. |
content_message | object | Same shape as POST /messages. |
Example
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+491791234567",
"consent_audit_log_id": 12345,
"content_message": {
"text": "👋 Du bist abgemeldet. Falls du's anders überlegst: einfach START zurück."
}
}' \
https://api.box.rcs.jetzt/v1/agents/7/messages/final
Error responses
| HTTP | When |
|---|---|
404 | The consent_audit_log_id is unknown to your tenant. |
403 | Audit log belongs to a different agent or phone, is not OPT_OUT, isn't acked, has already been consumed, or the 5-min window has expired. |
400 | Phone is outside the tenant's allowed country prefixes. |
422 | Content message validation failed (same shape as POST /messages). |
Testers
While an agent is in DRAFT, Google only
delivers messages to registered tester phone numbers. Use these endpoints to manage the
tester allow-list for each of your agents. Phones must be in the tenant's allowed country prefixes
(V1: +49 only).
/v1/agents/:agent_id/testers
Auth required
Lists testers registered locally for the given agent.
Example Request
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/agents/7/testers
Response (200)
{
"testers": [
{
"id": 12,
"agent_id": 7,
"phone": "+491791234567",
"status": "ACCEPTED",
"google_resource_name": "brands/acme-corp/agents/my-agent/testers/+491791234567",
"created_at": "2026-04-20T10:00:00Z",
"updated_at": "2026-04-20T10:05:00Z"
}
]
}
/v1/agents/:agent_id/testers
Auth required
Registers a phone number as a tester for the given agent. Forwards to Google Business Communications and stores the result locally. Google triggers an RCS invitation on the device.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | E.164 phone number (e.g. +491791234567). Must match tenant's allowed prefixes. |
Example Request
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+491791234567"}' \
https://api.box.rcs.jetzt/v1/agents/7/testers
Response (201)
{
"tester": {
"id": 12,
"agent_id": 7,
"phone": "+491791234567",
"status": "INVITED",
"google_resource_name": "brands/acme-corp/agents/my-agent/testers/+491791234567",
"created_at": "2026-04-20T10:00:00Z",
"updated_at": "2026-04-20T10:00:00Z"
}
}
Error — Phone not in allow-list (400)
Prefixes reflect the tenant's allowed_countries at request time (example shown for DE + AT):
{
"error": "Bad Request",
"message": "Phone number is not in the tenant's allow-list. Allowed prefixes: +49, +43"
}
/v1/agents/:agent_id/testers/:id
Auth required
Removes a tester both at Google and locally. If Google reports the tester as already gone (404), the local record is still removed.
Example Request
curl -X DELETE \
-H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/agents/7/testers/12
Response (204)
Empty response body.
/v1/agents/:agent_id/testers/:id/status
Auth required
Pulls the current invite status from Google for the tester and updates the local record. Use this to check whether a user accepted the invitation on their device.
Example Request
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/agents/7/testers/12/status
Response (200)
{
"tester": {
"id": 12,
"agent_id": 7,
"phone": "+491791234567",
"status": "ACCEPTED",
"google_resource_name": "brands/acme-corp/agents/my-agent/testers/+491791234567",
"created_at": "2026-04-20T10:00:00Z",
"updated_at": "2026-04-20T10:15:00Z"
}
}
Status values: INVITED, PENDING,
ACCEPTED, DECLINED,
UNKNOWN, NOT_FOUND.
Brands
A brand represents the legal entity sending RCS messages (e.g. Acme Ltd.).
Each tenant maps to one Google brand; agents live underneath it. Public endpoints
let customers read brand state. Mutating actions (sync, document upload, vendor calls)
are performed by Railslove operators via the admin UI.
Operator- und Vendor-Endpunkte siehe Internal API bzw. Vendor API am Seitenende.
/v1/brands
Auth required
Lists brands visible to the authenticated tenant.
Example Request
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/brands
Response (200)
{
"brands": [
{
"google_brand_id": "brands/abc",
"display_name": "Acme Ltd.",
"verification_state": "VERIFIED",
"synced_at": "2026-06-02T12:00:00Z"
}
]
}
/v1/brands/:google_brand_id
Auth required
Returns a single brand by its Google brand id (e.g. brands/abc).
Example Request
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/brands/brands/abc
Response (200)
{
"brand": {
"google_brand_id": "brands/abc",
"display_name": "Acme Ltd.",
"verification_state": "VERIFIED",
"synced_at": "2026-06-02T12:00:00Z"
}
}
/v1/brands/:google_brand_id/verification
Auth required
Returns the current brand-level verification state mirrored from Google.
Response (200)
{
"google_brand_id": "brands/abc",
"verification_state": "VERIFIED",
"verification_synced_at": "2026-06-02T12:00:00Z"
}
Launch
Launch is the per-carrier rollout. Once an agent is VERIFIED,
Railslove submits a launch request listing the target carriers and answering a questionnaire
about message volume, opt-in flow, and use case. Google then forwards the request to each
carrier; carrier states transition independently to LAUNCHED.
Operator- und Vendor-Endpunkte siehe Internal API bzw. Vendor API am Seitenende.
Operator checklist
[ ] Brand exists & synced from Google (GET /v1/brands)
[ ] Brand verification contact filled (partner + brand + website)
[ ] Supporting PDFs uploaded (1–5 PDFs, ≤50MB each)
[ ] Agent created at Google with all required fields
(logo, hero, color, privacy, terms, website, billing, region)
[ ] At least one Tester registered + accepted invite
[ ] STOP flow tested with tester (operator confirms)
[ ] Agent asset URLs HTTPS-reachable (UI button runs HEAD probe)
[ ] Verification submitted (requestVerification) → state VERIFIED
[ ] Carriers selected
[ ] Launch questionnaire complete
[ ] Launch submitted (requestLaunch) → poll getLaunch until LAUNCHED
/v1/agents/:id/launch
Auth required
Returns the agent's overall launch state plus a per-carrier breakdown. :id
is the local Agent ID (Rails bigint primary key). Use this to monitor rollout progress.
Response (200)
{
"status": "LAUNCHED",
"launch_details": [
{
"region": "regions/dt-germany",
"launch_state": "LAUNCH_STATE_LAUNCHED",
"update_time": "2026-06-01T08:30:00Z",
"comment": null
}
]
}
/v1/agents/:id/launch_questionnaire_draft
Auth required
Reicht die Launch-Questionnaire-Antworten als Customer ein. Jeder Aufruf überschreibt den vorherigen Draft. Railslove sichtet, übernimmt und sendet die Daten anschließend an Google.
Example Request
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"primary_contact_name": "Joe",
"primary_contact_email": "joe@example.com",
"opt_in_description": "User signs up on our website",
"opt_out_description": "Reply STOP",
"trigger_description": "Order confirmation",
"interaction_description": "Customer service",
"video_url": "https://example.com/demo.mp4",
"screenshot_url": "https://example.com/screenshot.png"
}' \
https://api.box.rcs.jetzt/v1/agents/<agent_id>/launch_questionnaire_draft
Response (201)
{
"agent_id": 19,
"google_agent_id": "brands/acme/agents/local-1",
"submitted_at": "2026-06-03T01:00:00Z",
"draft": {
"primary_contact_name": "Joe",
"primary_contact_email": "joe@example.com",
...
}
}
Validation errors (422)
primary_contact_email is required and must be a valid email.
video_url and screenshot_url are optional but
must begin with http:// or https:// when provided.
{
"error": "Validation failed",
"errors": { "primary_contact_email": "invalid_format" }
}
/v1/agents/:id/launch_questionnaire_draft
Auth required
Gibt den aktuell eingereichten (aber noch nicht von Railslove übernommenen) Draft zurück.
draft ist null, wenn nichts eingereicht wurde
oder Railslove den Draft bereits übernommen oder verworfen hat.
Response (200)
{
"agent_id": 19,
"google_agent_id": "brands/acme/agents/local-1",
"submitted_at": "2026-06-03T01:00:00Z",
"draft": {
"primary_contact_email": "joe@example.com",
"trigger_description": "Order confirmation"
}
}
Carriers
/v1/carriers
Auth required
Lists the German carriers RCS-Box has contracts with (mirrored from Google's regions catalog). Used when selecting targets for launch.
Example Request
curl -H "Authorization: Bearer $API_KEY" \
https://api.box.rcs.jetzt/v1/carriers
Response (200)
{
"carriers": [
{
"slug": "dt-germany",
"name": "Germany: DT",
"google_region_name": "regions/dt-germany",
"management_type": "GOOGLE_MANAGED"
}
]
}
Capabilities
/v1/agents/:agent_id/capabilities
Auth required
Checks whether a phone number is reachable via RCS for the given agent. Returns the list of RCS features that Google reports for the device. Use this before sending to avoid wasted billable sends to non-RCS numbers.
Query parameters
| Param | Description |
|---|---|
| phone | E.164 phone number to check, e.g. +491791234567. Must match the tenant's country allow-list. |
Example Request
curl -H "Authorization: Bearer $API_KEY" \
"https://api.box.rcs.jetzt/v1/agents/7/capabilities?phone=%2B491791234567"
Response — RCS-capable (200)
{
"rcs_enabled": true,
"features": [
"RICHCARD_STANDALONE",
"RICHCARD_CAROUSEL",
"ACTION_CREATE_CALENDAR_EVENT",
"ACTION_DIAL"
]
}
Response — not RCS-capable (200)
{
"rcs_enabled": false,
"features": []
}
Don't cache the result. RCS capability can change at any time (device swap, carrier change, user disabling RCS). Google recommends running the check immediately before sending. RCS-Box does not persist the response.
/v1/agents/:agent_id/capabilities/batch
Auth required
Checks reachability for up to 100 phone numbers in a single call. Useful for pre-filtering large recipient lists. Returns one result per input phone, in the same order as the request.
What this endpoint does NOT return: the per-number feature list.
Google's batch lookup only reports reachability (true/false).
If you need the supported feature set for a number, call the
single-number endpoint.
Request body
| Field | Description |
|---|---|
| phones | Non-empty array of E.164 phone numbers (max 100). Numbers outside the tenant's country allow-list are reported as skipped without contacting Google. |
Example Request
curl -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"phones":["+491791234567","+491791234568","+12025550100"]}' \
https://api.box.rcs.jetzt/v1/agents/7/capabilities/batch
Response (200)
{
"results": [
{ "phone": "+491791234567", "rcs_enabled": true },
{ "phone": "+491791234568", "rcs_enabled": false },
{ "phone": "+12025550100", "rcs_enabled": false, "skipped": "not_in_allowlist" }
]
}
When skipped is set, rcs_enabled is always false — we did not query Google for that number.
not_in_allowlist— the number is well-formed E.164 but outside the tenant's country allow-list.invalid_phone— the entry is not a non-empty string, is missing a leading+, or contains no digits. Use this to flag malformed input on the client side.
Error responses
| Status | When |
|---|---|
| 400 | phones missing, empty, not an array, or longer than 100 |
| 401 | Missing or invalid API key |
| 404 | Agent not found in your tenant |
| 422 | Google API error (rate-limited, brand misconfigured, etc.) |
Webhooks
Each tenant may configure a webhook_url (set by your administrator).
When Google sends an incoming user message for one of your agents, RCS-Box forwards the payload to that URL.
The shape matches Google's RCS Business Messaging webhook format. Opt-out / opt-in messages are also forwarded,
with an extra rcs_box_action field;
these forwards require an acknowledgement from your endpoint — see the
Opt-out / Opt-in section. Delivery events
(DELIVERED, READ) are also forwarded
as-is — without an rcs_box_action field — so you can
match them against your outbound-message log via event.messageId.
Request format
RCS-Box forwards every callback as an HTTP POST to your configured
webhook_url. The body is the raw JSON
payload — no form-encoding, no wrapper. The connection timeout is 30 seconds; slower
responses are retried via Sidekiq (up to 3× for ack'd flows; see
Opt-out / Opt-in).
| Property | Value |
|---|---|
| Method | POST |
| URL | Your tenant's webhook_url |
| Content-Type | application/json |
| X-RCS-Box-Signature | Hex HMAC-SHA256 over the body, keyed with your API key |
| Body | Raw JSON (the original Google payload, optionally enriched with rcs_box_action) |
| Timeout | 30 s |
| Expected response | 2xx for regular forwards; 200 + {"acknowledged": true} for opt-out/opt-in |
Example forwarded request
POST /your/webhook/path HTTP/1.1
Host: your-domain.example.com
Content-Type: application/json
X-RCS-Box-Signature: 5e7c9a…b3f1
{
"agent": "brands/acme-corp/agents/my-agent",
"senderPhoneNumber": "+491701234567",
"message": { "text": "Hello!" }
}
Signature header
Every forwarded request carries an X-RCS-Box-Signature
header. The value is a lowercase hex-encoded HMAC-SHA256 computed over the request body (the
JSON payload as serialized by RCS-Box), keyed with your API key. Verify it before trusting the payload.
# Ruby
require "openssl"
expected = OpenSSL::HMAC.hexdigest("SHA256", api_key, request.raw_post)
actual = request.headers["X-RCS-Box-Signature"]
reject unless ActiveSupport::SecurityUtils.secure_compare(expected, actual)
Event types
Google sends three callback shapes. You can distinguish them by the top-level keys present in the payload.
Only user messages are forwarded to your webhook_url;
delivery events stay internal.
| Top-level key | Meaning | Forwarded? |
|---|---|---|
| message | User reply containing text, suggestion postback, or media | Yes |
| userMessage | Alternate shape Google uses for the same thing | Yes |
| event | Delivery status (DELIVERED, READ) — updates the internal MessageLog only | No |
Opt-out / Opt-in
When a user replies with a message whose first word (case-insensitive, after trimming whitespace)
matches one of the keywords below, RCS-Box automatically updates the per-agent blacklist and forwards
the original payload to your
webhook_url with an extra
rcs_box_action field set to
"OPT_OUT" or "OPT_IN".
| Action | Recognized keywords | Effect |
|---|---|---|
| OPT_OUT | STOP, STOPP, ABMELDEN, ENDE, QUIT, UNSUBSCRIBE, NEIN | Phone added to blacklist immediately (fail-safe), then forwarded for tenant ack |
| OPT_IN | START, ANMELDEN, SUBSCRIBE, JA | Phone removed from blacklist immediately on START; tenant ack is audit-of-record |
Example forward (opt-out)
{
"agent": "brands/acme-corp/agents/my-agent",
"senderPhoneNumber": "+491701234567",
"userMessage": { "text": "STOP" },
"rcs_box_action": "OPT_OUT",
"consent_audit_log_id": 12345
}
The consent_audit_log_id is your handle for a one-time "goodbye"
send via POST /messages/final
— useful for the "Tschüss + STOP-Hinweis-Wiederholung"-Pattern that the per-agent blacklist
would otherwise block.
Matching rules: Only the first word of the user's message is matched against the keyword
list (after trimming whitespace, case-insensitively). The full original text — including anything after
the keyword — is preserved in the forwarded payload, so QR-code deeplinks of the form
"START <scan_token>" are recognized as opt-in and the token reaches
your endpoint via userMessage.text. Messages that contain a keyword later in
the sentence (e.g. "please stop spamming") are forwarded as regular user replies
without the rcs_box_action field. Repeated STOPs from the same number are idempotent.
Tenant acknowledgement (required)
For every forward with rcs_box_action set, your endpoint must respond
200 OK with the JSON body
{"acknowledged": true} within 30 seconds. The acknowledgement is what
proves to RCS-Box (and to your DSGVO audit trail) that you have processed the consent change in your own system.
HTTP/1.1 200 OK
Content-Type: application/json
{ "acknowledged": true }
| Tenant response | OPT_OUT result | OPT_IN result |
|---|---|---|
200 + {"acknowledged": true} |
Audit log marked acked. Phone stays blacklisted (already was). | Audit log marked acked. Phone was already removed from the blacklist when the START webhook arrived — the ack is the audit-of-record, not the trigger. |
| Missing/false ack body, 5xx, or timeout (> 30 s) | Phone stays blacklisted (fail-safe). Job retries up to 3× total (MAX_ATTEMPTS). |
Audit log marked failed. Phone stays de-blacklisted — the user already pressed START, that's the consent of record. Use the failed audit row to reconcile your CRM. Job retries up to 3× total (MAX_ATTEMPTS). |
Why we still require the ack: Your {"acknowledged": true} is what
tells RCS-Box that the consent change reached your CRM / email-list / downstream system. For OPT_OUT
this is critical — without the ack we retry, and the phone stays blacklisted as a fail-safe. For
OPT_IN the blacklist is removed as soon as the START webhook arrives, regardless of ack: the user
actively re-engaged, and a fragile tenant endpoint shouldn't keep them blocked. The ack is still required and
still retried; a missing ack leaves a consent_audit_logs row with ack_received: false
(plus populated error and incremented attempts), which is your signal to reconcile with your own system.
First-time OPT_IN (no prior blacklist entry)
OPT_IN handling is symmetric: even when the user has never been on your blacklist, a START message still
produces a consent_audit_logs row, still gets forwarded with
"rcs_box_action": "OPT_IN", and still requires your
{"acknowledged": true} response. The internal remove-from-blacklist step
runs unconditionally when the START webhook arrives and is a no-op when there's no prior entry; the audit log
is the part that captures the explicit consent. Use the forwarded event to register a first-time opt-in in your
own system — e.g. flag the contact as "consent given by RCS reply" in your CRM.
How to test as a tenant
End-to-end you need three things: a public URL that captures incoming HTTP requests, a configured
webhook_url on your tenant, and an
accepted tester number that can send real RCS messages to one of your agents.
1. Set up a capture URL
Open webhook.site — you get a unique URL
like https://webhook.site/<uuid>. Keep the tab open; every request the URL
receives appears live with full body and headers.
2. Configure your tenant webhook_url
Ask your administrator to set the webhook_url
on your tenant to the webhook.site URL from step 1. The setting lives in the admin tenant edit form.
3. Add and accept a tester
Pick an agent, register a tester phone number you control, and accept the invitation on that device. You can do this via POST /v1/agents/:agent_id/testers or in the admin UI.
4. Trigger callbacks
From the tester device, send the following messages to your agent:
| Send from tester | Expected at webhook.site |
|---|---|
| "hello there" | Forward without rcs_box_action |
| "STOP" (or ENDE, NEIN, ABMELDEN, …) | Forward with "rcs_box_action": "OPT_OUT" |
| "START" (or JA, ANMELDEN, …) | Forward with "rcs_box_action": "OPT_IN" |
| "please stop spamming me" | Forward without rcs_box_action — the substring is ignored |
Note about webhook.site & opt-in/opt-out: webhook.site captures requests but does not return
{"acknowledged": true}. STOP/START forwards are therefore retried up to 3
times total before the job gives up, and the matching consent_audit_logs row ends with
ack_received: false (populated error, incremented attempts).
The blacklist mutation itself still happens (OPT_OUT adds, OPT_IN removes)
regardless of ack — only the audit-of-record is affected by webhook.site's missing ack. To exercise the full
ack flow end-to-end, point webhook_url at an endpoint you control that responds
200 with {"acknowledged": true}.
5. Verify the signature
Each forward carries an X-RCS-Box-Signature
header. Recompute it locally with your API key over the raw request body (see signature header).
If the values match you know the payload is from RCS-Box and untampered.
RCS testing constraint: Google only delivers user messages from accepted tester
numbers while an agent is in DRAFT/PENDING state.
A regular phone you have not registered as a tester will not produce any callback — you will see no
request hit webhook.site. Always test from the tester device.
Error Handling
All errors return a consistent JSON format with an error
and message field.
{
"error": "Unauthorized",
"message": "API key required"
}
HTTP Status Codes
| Code | Meaning | Common Cause |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Agent created or message accepted |
| 400 | Bad Request | Missing/invalid parameters, non-German phone number |
| 401 | Unauthorized | Missing or invalid API key |
| 402 | Payment Required | Plan quota exceeded — see error_key for the specific reason (e.g. quota_exceeded_free_tier) |
| 403 | Forbidden | Tenant is inactive or recipient is blacklisted (STOP-opt-out or admin-blocked) |
| 404 | Not Found | Agent or resource does not exist — also returned when accessing another tenant's resources |
| 422 | Unprocessable | Agent not launched, Google API rejection |
| 429 | Rate Limited | Too many requests, retry after backoff |
| 500 | Server Error | Internal error, contact support |
Rate Limits
Requests to the Google RCS API are subject to rate limiting. RCS-Box automatically retries rate-limited requests (HTTP 429) with exponential backoff up to 3 times.
Retry strategy: Initial interval of 0.5s with a backoff factor of 2x and 50% randomization. Retries also apply to server errors (500, 502, 503, 504).
Phone Number Format
All phone numbers must be provided in E.164 format. Each tenant has an allow-list of country prefixes;
V1 defaults to Germany only (+49). Formatting characters
(spaces, dashes) are stripped before validation; numbers outside the tenant's allow-list are rejected with HTTP 400.
| Input | Normalized | Accepted? |
|---|---|---|
| +49 170 123 4567 | +491701234567 | Yes |
| +49-170-123-4567 | +491701234567 | Yes |
| +43 677 1234567 | +436771234567 | No (AT) |
| +1 202 555 0100 | +12025550100 | No (US) |
Internal API — Operated by Railslove
Endpunkte in diesem Block werden ausschließlich von Railslove im Auftrag des Kunden aufgerufen. Sie sind hier zur Transparenz dokumentiert; Kunden haben darauf keinen direkten Zugriff. Wende dich an support@railslove.com, um eine dieser Operationen auszulösen.
Brand admin
/admin/brands/:id/sync
Internal (Railslove only)
Re-fetches the brand from Google Business Communications and refreshes the local mirror. Triggered by Railslove operators after Google updates the brand contact or verification status.
/admin/brands/:id
Internal (Railslove only)
Updates local brand metadata (display name, partner contact, website). Does not write to Google; use the vendor endpoints for that.
/admin/brands/:id/documents
Internal (Railslove only)
Uploads a supporting PDF (1–5 per brand, ≤50 MB each) used by Google to verify the legal entity. Stored in encrypted blob storage and attached to subsequent verification requests.
/admin/brands/:id/documents/:doc_id
Internal (Railslove only)
Removes an uploaded brand PDF.
Verification admin
Verification is the process where Google checks that an agent legitimately belongs to its brand. It requires the brand contact, supporting documents, and an agent in a complete state (logo, hero, color, privacy URL, terms URL, website, billing, region). The endpoints below are driven by Railslove on the customer's behalf.
/admin/brands/:id/verification/submit
Internal (Railslove only)
Submits the brand for verification. Internally picks the brand's first agent (Google
caches verification per-brand), calls vendor :requestVerification
first, then uploads the brand's PDF attachments onto the verification record. Operator-only;
once-per-brand.
/admin/brands/:id/verification/refresh
Internal (Railslove only)
Polls Google for the latest verification status of the brand and updates the local mirror. Refresh ist manuell — Operator klickt im Admin auf "Refresh from Google". Eine Background-Polling-Variante ist als Follow-up geplant.
Launch admin
/admin/agents/:id/launch/submit
Internal (Railslove only)
Submits the launch request to Google. Gated by readiness (verified + carriers selected +
questionnaire complete + assets reachable). Calls vendor :requestLaunch.
/admin/agents/:id/launch/add_carriers
Internal (Railslove only)
Adds additional carriers to an already-launched agent. Calls vendor PATCH to amend the launch document with the new carrier list.
/admin/agents/:id/launch/refresh
Internal (Railslove only)
Polls Google for the latest per-carrier launch state and updates the local mirror.
/admin/agents/:id/launch/questionnaire
Internal (Railslove only)
Updates the launch questionnaire fields (use case, opt-in flow, monthly volume, sample messages) stored locally and sent with the launch request.
/admin/agents/:id/launch/readiness
Internal (Railslove only)
Records operator confirmation of manual checklist items (STOP flow tested, tester accepted, etc.) before submitting the launch.
/admin/agents/:id/launch/probe_assets
Internal (Railslove only)
Runs a HEAD probe against the agent's asset URLs (logo, hero, privacy, terms, website) to confirm HTTPS reachability and reasonable content types. Surfaces failures inline in the admin UI.
Carriers admin
/admin/carriers/sync
Internal (Railslove only)
Re-fetches the carrier/regions catalog from Google and refreshes the local mirror. Run manually after Google announces new carriers.
Analytics-Ingestion (Device/Browser)
Auf deiner Landing-/Redirect-Page kannst du Device- und Browser-Daten clientseitig erfassen und an RCS-Box senden. Wir parsen den User-Agent server-seitig und speichern nur aggregierte, anonyme Felder (kein IP, kein roher User-Agent). Den Write-Key und die erlaubten Origins findest du im Admin auf der Agent-Seite. Rufe das Snippet erst nach Einwilligung auf deiner eigenen Seite auf.
<script>
navigator.sendBeacon(
"https://<deine-rcs-box-domain>/api/v1/analytics/events",
new Blob([JSON.stringify({
key: "pk_DEIN_WRITE_KEY",
campaign: "summer-2026", // optional
event_type: "page_view", // z. B. page_view, cta_click
screen_w: screen.width,
screen_h: screen.height,
viewport_w: window.innerWidth,
viewport_h: window.innerHeight,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
referrer: document.referrer
})], { type: "text/plain" })
);
</script>
Wir senden als text/plain, damit der Beacon eine CORS-safelisted Simple-Request ist (kein Preflight); der Body ist trotzdem JSON und wird server-seitig geparst.
Antwort: 202 Accepted. Origin muss in der Agent-Allowlist stehen, sonst
403; ungültiger/deaktivierter Key ⇒ 401.
Vendor API — Google Business Communications
Die folgenden Endpunkte ruft Railslove gegen Google's Business Communications API auf — sie laufen NICHT durch die RCS-Box. Sie sind hier dokumentiert, damit Kunden sehen, was im Hintergrund passiert. Vollständige Referenz: developers.google.com.
Brand vendor
businesscommunications/v1/brands
Vendor (Google)
Underlying Google Business Communications call used by /admin/brands/:id/sync.
Not callable by customers; documented for transparency.
businesscommunications/v1/{brand}
Vendor (Google)
Fetches a single Google brand resource. Used by sync to populate display_name
and verification state.
businesscommunications/v1/brands
Vendor (Google)
Manual one-off creation of a new brand at Google. Performed by a Railslove operator during
tenant onboarding; the resulting google_brand_id is then stored on the tenant.
Verification vendor
businesscommunications/v1/{agent}:requestVerification
Vendor (Google)
Underlying Google call that asks Google to verify the agent.
businesscommunications/v1/{agent}/verification
Vendor (Google)
Reads the current verification document at Google. Used by refresh.
businesscommunications/upload/v1/{agent}/attachments
Vendor (Google)
Uploads a single PDF attachment to Google. Called per document during submission, after
:requestVerification has created the verification record — the
upload docks the BAL onto that record.
businesscommunications/v1/{agent}/attachments/:id
Vendor (Google)
Removes a previously uploaded attachment at Google.
Launch vendor
businesscommunications/v1/{agent}:requestLaunch
Vendor (Google)
Underlying Google call to launch an agent across the listed carriers. Body includes the questionnaire payload.
businesscommunications/v1/{agent}/launch
Vendor (Google)
Reads the current launch document at Google, including per-carrier state. Used by refresh.
businesscommunications/v1/{agent}/launch
Vendor (Google)
Amends an existing launch document (e.g. to add carriers). Called by add_carriers.
Carriers vendor
businesscommunications/v1/regions
Vendor (Google)
Underlying Google call returning the carriers/regions catalog.