What it allows
- Definitions: the document of a survey (pages, questions, conditions and informational blocks). It is saved as a draft, compiled in a preview and published as a numbered, immutable version.
- Campaigns: an edition of a published version for an audience, with dates, a modality and privacy rules. It is saved as a draft and taken through its lifecycle: schedule, open, pause, resume, close, cancel and archive.
- Results: the aggregate that the privacy policy of an own campaign allows.
There are no routes for exports, individual responses or participants. There are no routes to delete objects either.
Authentication and grant
Requests authenticate like those of the Data API: an OAuth 2.0 token or an API key, sent as Authorization: Bearer. The read:surveys scope reads the own objects. The write:surveys scope also writes, but it neither publishes nor reads results: can_publish and can_read_results are granted separately and default to false.
The scope is not enough. The company administrator grants the integration, in Empresa › Integraciones of the application, the data permission of the surveys domain. That permission requires the confidential field group and, to name people in an audience, also people. Without the permission or the scope, the API responds 403 insufficient_scope. Whoever grants or widens the permission needs the equivalent permissions in the application for what is delegated, and remains the responsible person: what the integration prepares is recorded under that name to keep the duties separate.
The grant is read again on every request: whatever is revoked stops applying immediately. A missing extension is the same as an empty one and everything is denied.
Field paths
| Path | What it allows |
|---|---|
definitions | Draft and preview definitions, and publish them with can_publish. |
campaigns | Draft campaigns and take them through their lifecycle. Audiences and contexts are only granted with this path. |
Without explicit paths, both are granted. The permission must be a write permission (access: write) to write.
What the grant limits
| Permission field | Effect |
|---|---|
allowed_version_ids (up to 200) | Published versions of other definitions of the company that the integration can use in a campaign. Versions of its own definitions do not need to be granted. |
allowed_group_ids (up to 100) | Groups it can use in a selected audience. |
allowed_user_ids (up to 200) | People it can use in a selected audience. It requires the people field group. |
allowed_channel_ids (up to 100) | Communications channels it can use as the context of a campaign. |
allowed_modalities | Modalities it can use: confidential and identified. There is no anonymous modality. |
can_create | Create new definitions and campaigns. |
can_publish | Publish versions and schedule, open and resume campaigns. |
can_read_results | Read the aggregated results. |
allow_tenant_audience | tenant audience, which reaches the whole company. |
Contract version
Every request can send X-Surveys-Contract: 1. Without the header, version 1 applies, and any other value responds 406 unsupported_contract without touching anything. Responses carry X-Surveys-Contract: 1, Cache-Control: private, no-store and the body { "contract_version": 1, "data": … }.
Routes
All of them hang from https://daas.masnet.com/api/v1/integrations/data/surveys. Every write requires Idempotency-Key, previews included.
| Method and route | Use | Requires | Success |
|---|---|---|---|
PUT /definitions/{external_id} | Creates or updates the draft of a definition. | definitions. Creating requires can_create | 201 on create, 200 on update |
GET /definitions/{external_id} | Reads an own definition. | Read | 200 |
POST /definitions/{external_id}/preview | Compiles the draft and issues the digest that publish consumes. It does not change the draft. | definitions | 200 |
POST /definitions/{external_id}/publish | Publishes the draft as a version. | definitions and can_publish | 200 |
PUT /campaigns/{external_id} | Creates or updates the draft of a campaign. | campaigns. Creating requires can_create | 201 on create, 200 on update |
GET /campaigns/{external_id} | Reads an own campaign. | Read | 200 |
POST /campaigns/{external_id}/launch-preview | Computes the launch summary. It never lists people. | campaigns | 200 |
POST /campaigns/{external_id}/transition | Changes the state of the campaign with an action. | campaigns. schedule, open and resume require can_publish | 200 |
GET /campaigns/{external_id}/results | Reads the aggregated results. | can_read_results | 200 |
Bodies are JSON and are rejected if they carry an unknown field. A field about company, authorship, environment, ownership, privileges, permissions, approval, cohort, state or responses is rejected with its own code and is not ignored. Previews and commands carry the body {}, while publish and transition carry their own.
Every successful response carries the object in data and, when it applies, the ETag header. A creation also responds Location, and the repetition of an operation already applied responds Idempotency-Replayed: true.
Identity and concurrency
The external_id in the path is the stable identifier of the object in your system: 1 to 128 ASCII characters that start with a letter or digit and allow . _ - (^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$). It identifies the object within the namespace of the integration, the environment and the kind: two integrations can use the same external_id, and an object of another integration, environment or company, or one that today's grant no longer covers, responds 404 unavailable, the same as a nonexistent one.
| Mechanism | How it is used |
|---|---|
source_revision | An integer greater than or equal to 1 that your system increases with every change of the object. The API stores it as the last accepted revision. |
Idempotency-Key | 8 to 128 characters A-Z a-z 0-9 . _ : -, one per intent. The same key with the same request (route, body and preconditions) returns the previous result without repeating the effect and sets Idempotency-Replayed: true. With another request it responds 409 idempotency_conflict. It is kept for 72 hours. |
If-None-Match: * | Mandatory to create. Without it, 428 precondition_required. |
If-Match | Mandatory to update, publish and change the state, with the current etag of the object (the ETag header or the etag member). Another value responds 409 version_conflict. Previews do not require it. |
What a PUT responds
| Situation | Result |
|---|---|
| The object does not exist | With If-None-Match: * and can_create, it creates it and responds 201. Without the header, 428 precondition_required. |
source_revision greater than the last accepted one | With If-Match and the current etag, it applies the change and responds 200. With If-None-Match, 409 already_exists. A campaign that is no longer a draft responds 409 campaign_not_draft. |
Same source_revision and same body | It returns the current view without writing. |
Same or lower source_revision, with another body | 409 source_revision_conflict, with details.current_source_revision. |
| A person took control of the object | 409 manual_control, with no effect. |
On a timeout or a 503, repeat the request with the same Idempotency-Key. Publishing a draft identical to the latest version returns that version without creating another. See idempotency_key_required, idempotency_conflict, version_conflict, source_revision_conflict.
Definitions
| Field | Description |
|---|---|
source_revision | Required. An integer greater than or equal to 1. |
title | Required. 1 to 160 characters. |
owner_label | Optional. Label of the responsible area, up to 120 characters. |
allowed_modalities | Optional. Modalities that the definition admits, among the granted ones. If omitted, all the granted ones. |
document | Required. The survey document, a JSON object of up to 512 KiB. |
The document
The document belongs to the contract schema_version 1 and rule_engine_version 1. Its members are schema_version, rule_engine_version, default_locale, locales, title and pages, which are required, and description and estimated_minutes, which are optional. Texts are objects by language, such as { "es": "…" }. Each page carries items of two kinds: question, with its type, and info, an informational block whose body uses the same format as the Communications content.
| Aspect | Detail |
|---|---|
| Question types | short_text, long_text, single_choice, multiple_choice, yes_no, integer, decimal, date, scale, matrix_single |
Conditions (visible_when, required_when, skip) | Closed language: all, any, not, answered, equals, in, gt, gte, lt and lte. |
| Questions | Up to 100 |
| Pages | Up to 25 |
| Options per question | Up to 100 |
| Predicates per document | Up to 200, with a condition depth of up to 8 |
| Size of the responses | 256 KiB, of which 16 KiB are reserved for metadata. The preview reports it in budget. |
A document that does not compile responds 422 invalid_rule with details.errors, up to 50 problems, and the preview reports the same ones with ok: false. Beyond the size of 512 KiB, the response is 422 field_length with details.max_bytes.
Preview and publication
POST /definitions/{external_id}/previewcompiles the draft and responds{ ok, digest, compiled_digest, allowed_modalities, budget, expires_at, requires_approval, environment, simulation }. Thedigestis the one thatpublishconsumes.POST /definitions/{external_id}/publishcarries{ "digest": "…" }andIf-Match, and requirescan_publishand a current preview. It creates the next version, which is immutable and carriesversion_no,digestandsurvey_version_id, the identifier that campaigns use.- The view of the definition reports
draft_state(draft,readyorapproved, which are draft states in the application),draft_digest,latest_version_noandlatest_version.
Campaigns
| Field | Description |
|---|---|
source_revision | Required. An integer greater than or equal to 1. |
survey_version_id | Required. UUID of a published version: one from the integration's own definitions or a granted one. A retired version responds 409 version_retired. |
title | Required. 1 to 160 characters. |
owner_label | Optional. Up to 120 characters. |
modality | Required. identified or confidential, among those that the version admits and the granted ones. |
audience | Required. { "mode": "selected", "group_ids": ["…"], "user_ids": [12] } with granted groups and people (up to 100 groups and 200 people), or { "mode": "tenant" } with allow_tenant_audience. |
context | Optional. { "channel_id": "…", "publication_external_id": "…" }: a granted channel and, if indicated, a Communications publication of the same integration and environment, which must be published. It requires the company to have Communications. |
starts_at, ends_at | Optional while it is a draft. RFC 3339 instants with an explicit zone. The end is after the start and the campaign lasts at most 365 days. Scheduling requires both, and opening requires ends_at. |
timezone | IANA time zone. By default, America/Santiago. |
allow_revisions | true lets a person correct their response. By default, false. |
retention_days | Integer from 30 to 365. If omitted, the company value applies. |
min_k | Integer from 10 to 1,000, not lower than the company minimum. If omitted, that minimum applies. It is the threshold below which figures are not published. |
- A
PUTonly writes drafts. Saving a draft discards the launch summary and the approval it had. - The view of the campaign reports
stateandeffective_state, which incorporates the clock: an open campaign whose end date has passed appears as closed. The states aredraft,scheduled,open,paused,blocked,closed,cancelledandarchived. It also reportsblocked_reason,audience,context, the dates,launch_digest,approved,cohort(fixed_atandsize) andresults_ready. - The audience in the view carries
user_countand only carriesuser_idsif the permission includespeople. The view never lists people or responses.
Launch summary
POST /campaigns/{external_id}/launch-preview only applies to a draft or blocked campaign. It responds { digest, expires_at, counts, warnings, context_state, cohort_fixed, requires_approval, snapshot, environment, simulation }: the cohort counts, the warnings, the state of the context and the summary that schedule, open and resume consume. It does not include people. The cohort is fixed when the campaign is scheduled or opened.
Lifecycle actions
| Action | From | To | Requires |
|---|---|---|---|
schedule | draft | scheduled | can_publish, the current digest, a future starts_at and both dates. The campaign opens by itself at starts_at |
open | draft or scheduled | open | can_publish, a future ends_at and, from draft, the current digest |
pause | open | paused | Write |
resume | paused or blocked | open, or the previous state if it was blocked | can_publish. From blocked, also the current digest |
close | open, paused or blocked | closed | Write. It publishes the final result |
cancel | draft, scheduled, open, paused or blocked | cancelled | Write |
archive | closed | archived | Write |
The body is { "action": "…", "digest": "…" }, with If-Match. An action that does not fit the state responds campaign_not_draft, campaign_not_open, campaign_paused, campaign_not_paused, campaign_blocked, campaign_closed, campaign_not_closed. A campaign blocked because its context changed or became unavailable (context_changed or context_unavailable) is not resumed through the API: revalidating the context is a decision for a person, and the revalidate_context action responds 422 invalid_field.
Previews and approval
- Publishing a definition, and scheduling, opening or resuming from
blockeda campaign, require thedigestof the latest preview issued on that object. If it is missing,409 preview_required. If it expired,409 preview_expired. If it is another one, or it was computed with another grant orsource_revision,409 preview_stale. - A preview lives 5 minutes, measured with the database clock. Repeating it with the same idempotency key does not extend its validity. Writing the object discards it, and an applied change consumes it.
- With dual control in the company, or if the object is confidential, the preview reports
requires_approval: trueand the credential is not enough: a person must approve the exact summary in the application. It must be a person other than the authors of the draft and the person responsible for the permission. If missing,409 approval_required(details.reasonisseparation_of_dutiesif the person is not valid). - Pausing, closing, cancelling and archiving require neither
can_publishnor a summary, but they do require write access and coverage by the grant.
Results
GET /campaigns/{external_id}/results requires can_read_results and an own campaign with an active binding. It delivers only the aggregate that the privacy policy allows: never rows, free texts, names or free filters. Every query is recorded under the same policy as the application.
state | When | Content |
|---|---|---|
not_started | The campaign is a draft or is scheduled. | Only the state. |
collecting | A confidential campaign is open, paused or blocked. | Only the state: no figures, counter or progress. |
live | An identified campaign is open, paused, blocked or cancelled. | Aggregates and denominators at the time of the query. |
ready | The campaign was closed or archived. | The final result, with snapshot_id and published_at. |
cancelled | A confidential campaign was cancelled. | Only the state. |
A result with figures carries privacy (min_k and rules_version), respondents and, in an identified campaign, denominators. Each question carries its groups, with the count of each option, n and suppressed: a figure from a group smaller than min_k is suppressed. A query consumes the read quota and the results quota.
Test integrations
A test-environment integration works in its own space, which people never see, and simulates instead of rejecting. publish and every transition validate the schema, the grant, can_publish and the summary, and respond 200 with simulation: true, would_state and requires_approval, plus would_version_no when publishing. They create no version, cohort or events, and they do not consume an approval. Results are a labeled example with simulation, fixture and notice, never real data.
The summary of a test preview carries the environment and the integration: it is not valid in production (409 preview_stale).
Limits and quotas
| Scope | Limit | When exceeded |
|---|---|---|
| Request body | 640 KiB | 413 payload_too_large |
| Document of a definition | 512 KiB | 422 field_length |
| Per integration, every minute | 120 reads, 30 writes and 10 results queries | 429 rate_limited with Retry-After: 60 |
| Per company, every minute | 600 reads, 150 writes and 30 results queries | 429 rate_limited with Retry-After: 60 |
| New objects per integration, drafts included | 300 per hour | 429 object_quota_exceeded with Retry-After: 600 |
| Live linked objects per integration | 10,000 | 409 binding_limit_reached |
| Scheduled, open, paused or blocked campaigns per company | 200 | 409 tenant_content_limit |
The per-minute quotas are measured with a shared counter and do not pass unmeasured: if the counter does not respond, the request is rejected with 503 quota_unavailable and Retry-After: 30, it is not executed and has no effect. Retry with the same Idempotency-Key. A results query consumes the read allowance and the results allowance, of the integration and of the company. The caps are maximums and can be set to a lower value. The other limits are measured in the database.
If the company is in read-only mode for maintenance, every new write responds 503 surveys_read_only with Retry-After: 60, with no effect. Reads and the repetition of operations already confirmed remain available. If the company plan does not include Surveys, the response is 403 module_unavailable.
Control and revocation
Every object reports control: integration while the integration governs it and local if a person took control in the application. With control: local, every external write responds 409 manual_control and has no effect. A person can return control from the application: the last accepted source_revision becomes the floor, and only a strictly greater revision moves forward, so even the same revision with the same body responds 409 source_revision_conflict.
- Removing the Surveys permission or revoking the integration releases its objects: they pass to local control, scheduled campaigns become
blocked(blocked_reasongrant_removedorintegration_revoked), pending jobs are cancelled, and the identity, the responses and the history are kept. - Granting the permission again does not reclaim what was released: the integration receives
409 manual_controluntil a person returns control. - The system revalidates against today's grant whatever the integration left scheduled before opening it. If the grant no longer covers the version, the audience, the channel, the modality or
can_publish, it blocks the campaign withauthority_lostand never opens it with an authority wider than the current one.
Errors
Errors use application/problem+json (RFC 9457) with type, title, status, detail, code and correlation_id. Some add a details member: fields (up to five fields), field, max_bytes, current_source_revision, reason, errors, supported or required. The detail never repeats the content sent and never reveals data of another company.
The complete list, with the cause and the action of each code, is in Errors.
Examples
The identifiers, the dates and the launch summaries are examples. The digest of the definition and those of its preview were computed with the API code over the document shown.
1. Create the definition
PUT /api/v1/integrations/data/surveys/definitions/erp-encuesta-abastecimiento HTTP/1.1
Host: daas.masnet.com
Authorization: Bearer <token>
X-Surveys-Contract: 1
Idempotency-Key: 41e7b0c9-85d2-4a36-9f1e-6c0d3a8b5e72
If-None-Match: *
Content-Type: application/json
{
"source_revision": 1,
"title": "Satisfacción con el área de abastecimiento",
"owner_label": "Área de abastecimiento",
"allowed_modalities": ["identified"],
"document": {
"schema_version": 1,
"rule_engine_version": 1,
"default_locale": "es",
"locales": [
"es"
],
"title": {
"es": "Satisfacción con el área de abastecimiento"
},
"description": {
"es": "Encuesta anual sobre la atención del área de abastecimiento."
},
"estimated_minutes": 3,
"pages": [
{
"id": "atencion",
"title": {
"es": "Atención recibida"
},
"items": [
{
"kind": "info",
"id": "intro",
"body": {
"format": "markdown",
"value": "Sus respuestas ayudan a **mejorar** el servicio."
}
},
{
"kind": "question",
"id": "calificacion",
"type": "scale",
"required": true,
"min": 1,
"max": 5,
"title": {
"es": "¿Cómo califica la atención del área de abastecimiento?"
},
"labels": {
"min": {
"es": "Muy mala"
},
"max": {
"es": "Muy buena"
}
}
},
{
"kind": "question",
"id": "recomienda",
"type": "yes_no",
"required": true,
"title": {
"es": "¿Volvería a solicitar apoyo al área?"
}
},
{
"kind": "question",
"id": "mejoras",
"type": "long_text",
"required": false,
"title": {
"es": "¿Qué podemos mejorar?"
},
"visible_when": {
"op": "lte",
"question": "calificacion",
"value": 3
}
}
]
}
]
}
}HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Cache-Control: private, no-store
X-Surveys-Contract: 1
ETag: "Zr7LkP3wN0cXq9TuV5aYhB"
Location: /api/v1/integrations/data/surveys/definitions/erp-encuesta-abastecimiento
{
"contract_version": 1,
"data": {
"external_id": "erp-encuesta-abastecimiento",
"kind": "definition",
"control": "integration",
"environment": "production",
"source_revision": 1,
"write_available": true,
"id": "a1c4e7d2-58b3-4f19-8e26-0d93b7c15a64",
"family_id": "5d0b8f31-92c7-4a6e-b1d4-3e7f60a9c2b8",
"title": "Satisfacción con el área de abastecimiento",
"owner_label": "Área de abastecimiento",
"draft_state": "draft",
"draft_digest": "8c754920f8b54805acfeee4c11e08c2f822550b20743cfac3b0d7e3e66e49a3a",
"allowed_modalities": ["identified"],
"latest_version_no": 0,
"latest_version": null,
"row_version": "1",
"etag": "\"Zr7LkP3wN0cXq9TuV5aYhB\"",
"created_at": "2026-11-04T14:40:12.508Z",
"updated_at": "2026-11-04T14:40:12.508Z"
}
}2. Preview the draft
The request is POST /definitions/erp-encuesta-abastecimiento/preview with the body {} and the same authentication and idempotency headers.
{
"contract_version": 1,
"data": {
"external_id": "erp-encuesta-abastecimiento",
"ok": true,
"digest": "260b7834294b3f41b753c06854f7aff2e3f8833594b737d9013b96c98b19aa67",
"compiled_digest": "03863614ef4cf7f8a63defc0ec182d7809d68b946c9440be3d7d8437009cf6f2",
"allowed_modalities": ["identified"],
"budget": { "max_bytes": 262144, "reserved_bytes": 16384, "used_bytes": 24051 },
"expires_at": "2026-11-04T14:46:40.000Z",
"requires_approval": false,
"environment": "production",
"simulation": false
}
}3. Publish the version
POST /api/v1/integrations/data/surveys/definitions/erp-encuesta-abastecimiento/publish HTTP/1.1
Host: daas.masnet.com
Authorization: Bearer <token>
X-Surveys-Contract: 1
Idempotency-Key: 7d2a9f06-3b84-4c15-a0e7-95c1b6e8d340
If-Match: "Zr7LkP3wN0cXq9TuV5aYhB"
Content-Type: application/json
{ "digest": "260b7834294b3f41b753c06854f7aff2e3f8833594b737d9013b96c98b19aa67" }4. Create the campaign
PUT /api/v1/integrations/data/surveys/campaigns/erp-campana-abastecimiento-2026 HTTP/1.1
Host: daas.masnet.com
Authorization: Bearer <token>
X-Surveys-Contract: 1
Idempotency-Key: e08b5c37-92a1-4d64-8c3f-1a7d0b6e9f25
If-None-Match: *
Content-Type: application/json
{
"source_revision": 1,
"survey_version_id": "c92e1b47-3a85-4d0f-a6b2-7f18d4e95c03",
"title": "Satisfacción con el área de abastecimiento, noviembre 2026",
"modality": "identified",
"audience": { "mode": "selected", "group_ids": ["7b3e5a90-2c14-4e8d-a5f7-91d6c0b48e23"] },
"starts_at": "2026-11-09T09:00:00-03:00",
"ends_at": "2026-11-20T18:00:00-03:00",
"timezone": "America/Santiago"
}{
"contract_version": 1,
"data": {
"external_id": "erp-campana-abastecimiento-2026",
"kind": "campaign",
"control": "integration",
"environment": "production",
"source_revision": 1,
"write_available": true,
"id": "e4a7d036-1b58-4c92-9f3d-6a20b8c71e45",
"survey_id": "a1c4e7d2-58b3-4f19-8e26-0d93b7c15a64",
"survey_version_id": "c92e1b47-3a85-4d0f-a6b2-7f18d4e95c03",
"title": "Satisfacción con el área de abastecimiento, noviembre 2026",
"owner_label": null,
"modality": "identified",
"state": "draft",
"effective_state": "draft",
"blocked_reason": null,
"audience": { "mode": "selected", "group_ids": ["7b3e5a90-2c14-4e8d-a5f7-91d6c0b48e23"], "user_count": 0 },
"context": null,
"starts_at": "2026-11-09T12:00:00.000Z",
"ends_at": "2026-11-20T21:00:00.000Z",
"timezone": "America/Santiago",
"allow_revisions": false,
"retention_days": 180,
"min_k": 10,
"launch_digest": null,
"approved": null,
"cohort": { "fixed_at": null, "size": null },
"opened_at": null,
"closed_at": null,
"cancelled_at": null,
"archived_at": null,
"results_ready": false,
"state_seq": "1",
"etag": "\"Hs2VdM8eJ4gTn1QxC6bWyK\"",
"created_at": "2026-11-04T14:52:30.221Z",
"updated_at": "2026-11-04T14:52:30.221Z"
}
}5. Schedule the campaign
First POST /campaigns/erp-campana-abastecimiento-2026/launch-preview is requested, and its digest goes in the action.
POST /api/v1/integrations/data/surveys/campaigns/erp-campana-abastecimiento-2026/transition HTTP/1.1
Host: daas.masnet.com
Authorization: Bearer <token>
X-Surveys-Contract: 1
Idempotency-Key: 5c91d8a3-0e47-4b2f-96d1-a3f7e2b04c68
If-Match: "Hs2VdM8eJ4gTn1QxC6bWyK"
Content-Type: application/json
{
"action": "schedule",
"digest": "9e07c3b5a1d84f6270b3e5c98a1d47f06b2e8c35d9a07f14e6b3c0a28d5f9174"
}6. Read the results of a confidential campaign in progress
The request is GET /campaigns/erp-campana-clima-2026/results.
{
"contract_version": 1,
"data": {
"external_id": "erp-campana-clima-2026",
"contract_version": 1,
"server_now": "2026-11-12T13:05:44.912Z",
"modality": "confidential",
"state": "collecting"
}
}An expired preview
HTTP/1.1 409 Conflict
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://masnet.com/developers/errors/preview_expired",
"title": "Conflicto",
"status": 409,
"detail": "El recurso cambió o la operación no corresponde a su estado.",
"code": "preview_expired",
"correlation_id": "7e3c0b92-6d14-4a85-b1f7-20c9d5a8e413"
}Quota unavailable
HTTP/1.1 503 Service Unavailable
Content-Type: application/problem+json; charset=utf-8
Retry-After: 30
{
"type": "https://masnet.com/developers/errors/quota_unavailable",
"title": "Cuota no disponible",
"status": 503,
"detail": "No se pudo medir la cuota; la solicitud no se ejecutó.",
"code": "quota_unavailable",
"correlation_id": "b27e90c4-1d63-4a58-8f0b-7c5a2e9d1f36"
}