Groupilot Docs

Sync your sequences

Push sequences from your platform into Groupilot with PUT /v1/sequences/sync, idempotently, upload media first, and understand what a resync changes on planned sends.

This is the endpoint built for platforms that write the messages and want Groupilot to send them: webinar builders, email tools, AI generators. One call saves the whole sequence. Calling it again with the same ids updates it. Reference: Sequences.

The call

PUT /v1/sequences/sync
Authorization: Bearer gp_live_…
Content-Type: application/json
Idempotency-Key: <uuid>

{
  "external_id": "ext_seq_8812",
  "name": "Sunday webinar 8 pm",
  "messages": [
    { "external_id": "ext_msg_1", "timing": "J-1 10:00", "type": "text",
      "text": "See you {J:jour} at 8 pm. Block the slot.", "ghost_mention": false },
    { "external_id": "ext_msg_2", "timing": "H-1", "type": "image",
      "media_url": "https://files.example.com/visual.jpg", "text": "One hour to go" },
    { "external_id": "ext_msg_3", "timing": "J0 19:55", "type": "text",
      "text": "We are live in 5 minutes: https://…", "ghost_mention": true }
  ],
  "apply_to_pending_jobs": true
}

Rules

  • external_id identifies the sequence in your system (1 to 200 characters). First call creates, next calls update. Unique per account.
  • Each message has its own external_id (required here, 1 to 200 characters), unique inside the sequence. Groupilot matches on it: unknown ids are created, known ids updated, ids absent from the list are removed. Sending the same body ten times changes nothing.
  • The order of the array is the order in the app.
  • 200 messages per sequence, 200 sequences per account.
  • Message fields: type (text, image, video, voice, document, poll, community_update), timing, text (10,000 characters max), media_url, poll, ghost_mention, link_preview (text messages only, default true; false sends the text without the preview card of its link), community_update (required on a step of type community_update, which changes the name or description of the community instead of posting a message).
  • media_url accepts any URL the WhatsApp provider can fetch. Uploading first through Media is recommended: format and size are checked there, whereas a bad external file only surfaces at send time.
  • timing: J-1 10:00 (day offset, then hours and minutes on two digits) or H-1 (hour offset, never H+0). Validated server side against the Timing pattern of the contract.
  • Date variables {J:jour}, {J:date}, {J:mois}, with an optional day offset such as {J+1:date}, are resolved at send time in French.
  • ghost_mention only on text. Keep it for one or two key messages.
  • Polls: poll: { question, options, select_count }, select_count 0 for several answers, 1 for one.

What happens to planned sends

A sequence can be linked to several events. Each event has occurrences, and each occurrence has one planned send per message, frozen when it was planned.

With apply_to_pending_jobs: true (default), after saving, Groupilot resyncs every linked event:

Planned sendResult
Pending, in the future, never attempted, not edited by handReplaced by the new content and timing
Already sent, in progress, failed or being retriedLeft as is, counted in locked_skipped
Edited by hand in the app (override)Left as is, counted in overrides_kept
Time already passedLeft as is, counted in locked_skipped

With apply_to_pending_jobs: false, the sequence is saved and nothing moves. Call POST /v1/sequences/{id}/resync later, with the event_ids you want (every linked event by default), after a preview with GET /v1/sequences/{id}/resync which lists, per linked event, eligible, jobs_to_resync, overrides_kept and locked_skipped.

A sequence linked to no event is just saved: resync is null in the response.

The response

{
  "sequence": { "id": "9f1c…", "external_id": "ext_seq_8812", "name": "Sunday webinar 8 pm", "messages": [ … ], "linked_events": [{ "id": "…", "name": "Sunday webinar", "status": "active" }], "created_at": "…" },
  "created": false,
  "messages": { "created": 1, "updated": 1, "deleted": 0, "unchanged": 1 },
  "resync": { "events_resynced": 1, "canceled_jobs": 4, "created_jobs": 6, "overrides_kept": 1, "locked_skipped": 2 }
}

created is true the first time this external_id is seen.

PUT /v1/events/{event_id}/sequence
{ "sequence_id": "9f1c…" }

The response is { "event": { … }, "created_jobs": 12 }. Linking replaces the pending sends of the future occurrences of that event. Sends whose time already passed are skipped. Later occurrences (recurring events, dates added by hand) get their sends automatically. { "sequence_id": null } unlinks.

Unit edits

POST /v1/sequences/{id}/messages, PATCH /v1/sequences/{id}/messages/{message_id}, DELETE …/messages/{message_id} and PUT …/messages/order do one change each. By default each one resyncs the linked events (resync: true); pass resync: false to batch several changes, then call POST /v1/sequences/{id}/resync once. Each answers with the updated sequence and the resync result.

DELETE /v1/sequences/{id} is refused with 409 while an active or paused event uses the sequence. With ?force=true, the events are unlinked and their pending sends canceled.

Media

A message with an image, a video, a voice note or a document needs a media_url. Any URL the WhatsApp provider can fetch is accepted. Uploading through the media endpoint first is recommended: format and size are checked there, whereas a bad external file only surfaces at send time. Two ways to do that.

Upload in two steps

Start the upload

POST /v1/media/uploads
{ "kind": "image", "filename": "visual.jpg" }
{ "id": "…", "upload_url": "https://…", "expires_at": "2026-11-20T10:10:00+01:00", "max_bytes": 16777216, "allowed_extensions": ["jpg", "jpeg", "png"] }

kind is image, video, voice or document.

PUT the file to upload_url

Send the raw file body with a Content-Type header, within 10 minutes. The URL works once.

Finish

POST /v1/media/uploads/{id}/complete
{ "id": "…", "kind": "image", "media_url": "https://…/visual.jpg", "bytes": 182044, "content_type": "image/jpeg" }

Groupilot checks the type and the size. A file outside the limits is deleted and the call fails. Use media_url in your messages.

Import from a public URL

POST /v1/media/import
{ "kind": "video", "url": "https://files.example.com/teaser.mp4", "filename": "teaser.mp4" }

Groupilot downloads the file itself (HTTPS, public host, 40 MB max) and answers with the same Media object.

Limits per kind: image 16 MB (jpg, jpeg, png), video 40 MB (mp4, mov, 3gp), voice 16 MB (ogg, m4a, wav, mp3), document 100 MB. 50 uploads per day per account.

Idempotency and limits

  • Send an Idempotency-Key header: a retried request after a timeout returns the stored response instead of running twice (24 hours). Same key with a different body returns 422.
  • sync is limited to 10 calls per minute per project. Debounce the saves on your side.
  • Validation errors come back as 400 with field pointing at the message, for example messages[1].timing.

Recipe: your users edit in your tool, Groupilot sends

  1. Store Groupilot's sequence.id next to your sequence after the first sync.
  2. On every save in your tool, debounce a few seconds, then call sync with the full list of messages.
  3. Show the resync counts to your user: "6 planned messages updated, 1 edited by hand kept".
  4. When your user creates a webinar, call POST /v1/events, add its date with POST /v1/events/{id}/occurrences, link the sequence with PUT /v1/events/{id}/sequence, and show the smart_link of the event.

On this page