Feedback items

POST /feedback_portal/{portalId}/feedback

Changed on

Create a feedback item, attributed to the person who submitted it.

Discovering what to send. Do not hard-code a field list. GET /feedback_portal/{portalId}/forms returns each form with its categories and which fields it requires, and GET /feedback_portal/{portalId}/custom_fields returns the field definitions and their allowed values. Both are readable with any key that can read the portal, and a form definition is the same for every visitor — so a client can render a form dynamically and never drift from what the portal actually defines.

Attribution. Send owner (the submitter's email) and, if you have one, submitterId — your own id for that person, which is the stronger key — plus submitterName, without which the portal can only show the local part of the address. A submitter the portal has not seen is created as a portal user who holds no password and can never sign in; they exist so the feedback is attributed to a real person and so their next submission resolves to the same one. A submission that MATCHES an existing submitter also refreshes their stored name and notification address from what you sent, so a rename or an address change on your side needs no separate reconciliation. Omit all of them, or send anonymous: true, and the item is attributed to the portal's anonymous submitter instead. Attributed submissions require pass-through identity to be enabled on the portal.

Retries may duplicate the ITEM, never the person. There is no idempotency key. If a request times out and you re-send it, the submitter resolves to the same person — that is guaranteed — but you may get a second feedback item, which is visible in the portal and deletable. Guard on your own side if a duplicate item matters to you.

Which failures to retry. 503 means the request was fine and Craft could not complete it: retry it. Any 4xx means the request itself is wrong — retrying will not help, so raise it instead. Craft never guesses that two submissions are the same one, so identical content is never deduplicated.

A second credential, for submitting from a browser. This route also accepts Authorization: Bearer <token> in place of x-api-key — a short-lived token your own server signs with your feedback portal's widget secret (scheme portal_token). It exists because no credential that reaches a browser can be kept secret, so the page carries a signed claim about who is submitting rather than a key. On that credential the identity is taken from the token's claims (sub, email, name, company, iss) and owner, submitterId, issuer, submitterName, anonymous and company in the BODY are rejected with 422 — the page is under the submitter's control and the token is not. The token is good for one hour from iat whatever its exp says, covers as many submissions as the person makes inside that hour, and is not single use. A 401 with error: "token_expired" means fetch a new one and retry; error: "token_invalid" will never succeed on retry.

Before you build the signing half, a portal can issue a development key (Authorization: Bearer cwk_dev_…) which is sent as-is with the identity fields in the body, accepted only from an origin the administrator registered as development. Everything it creates is marked as development in the portal's user list. It is a different key from the production one, so what you paste into a page while building can never sign a real submission.

What to send, and how to know it is right. GET /feedback_portal/{portalId}/widget/{widgetId}/config returns the widget's categories and the portal's own fields with their required flags, so a form can be rendered from the portal's definition and a missing field caught before posting rather than by a rejection here.

A stale importanceId does not cost you the submission. On either widget credential, an importance id the portal no longer has — an administrator can delete one while your page is open — is dropped and the portal's default applied, rather than refused. On x-api-key it is a 422 listing the valid ids, because that caller can re-read them and retry.

No cookie is set and no Craft session exists, on this call or on the config read. Worth stating as a property rather than an implementation detail: nothing about the person submitting is stored in their browser by Craft, which is usually the question a customer's privacy review asks first.

Request

  • Base URL
    https://api.craft.io
  • URL
    https://api.craft.io/feedback_portal/{portalId}/feedback
  • Auth
    API key in header x-api-key

Path parameters

portalIdstring required

Request body

workspaceIdstring
titlestring required
descriptionstring
ownerstring

Email address of the person who submitted the feedback.

Resolved against this portal, in order: an administrative user of the portal, then an existing portal user with that address. If neither matches, a portal user IS CREATED for the address — they hold no password and can never sign in, they simply exist so the feedback is attributed to them and so their next submission resolves to the same person. The new user is homed in company when you send one. Otherwise the company is inferred from the address: an existing portal company whose people already use that email domain, else a new company named for the domain. An address at a free-mail or disposable domain infers nothing — gmail.com is not a customer — so those submitters go to the portal's "Individual Submitters" company, which is created on first use and holds everyone whose company is not determinable.

Omit it and the feedback is attributed to the portal's anonymous submitter (the same as sending anonymous: true).

This is NOT a user ID — sending anything that is not an email address returns 422 (owner must be a valid email address), like every other value error here.

categoryIdstring
labelsstring[]
widgetIdstring

The widget this submission came from — send it on every widget submission.

It names the configuration the form was rendered from, so the settings an administrator made per widget are applied to the submission rather than having to be echoed back by the page. Today that is the importance: a widget whose form does not ask (hideImportance) submits no importanceId, and the widget's own defaultImportanceId is applied here instead of the portal's default. Anything else that becomes per-widget will be applied the same way, with no change needed in the page.

A widget id this portal does not have is a 422 on an API key and is ignored on a widget credential. An administrator can delete a widget while a page is open, and a submission somebody has already typed must not be lost to it — the portal's default applies, exactly as a submission naming no widget would get. An API key can re-read and retry, so it is told.

Omit it and nothing per-widget is applied; the portal's own defaults stand. An explicit importanceId always wins over the widget's.

importanceIdstring

Portal importance — numeric ID OR exact name (case-insensitive). When omitted, the portal's isDefault: true importance is used (see GET /feedback_portal/{portalId}/importances) — or, when widgetId names a widget that hides the importance question, that widget's defaultImportanceId.

companystring

Customer company — numeric ID OR exact name (case-insensitive). Resolve via GET /feedback_portal/{portalId}/companies.

Sets the feedback's company, and is also the company a submitter created from owner is placed in. A value that matches no company in this portal is a 422 listing the valid ones; it is never created and never silently ignored.

Only settable on restricted portals — a public portal attributes everything to its default company, so sending it there is a 422.

anonymousboolean

Submit without attributing the feedback to a person: the item is attributed to the portal's anonymous submitter, so it reads as anonymous everywhere.

This is the explicit form of what omitting owner already does — send it when you want the intent to be visible in the request rather than inferred from an absent field.

Mutually exclusive with owner — sending both returns 422. company is still honoured, so anonymous feedback can still be grouped under a customer company.

submitterIdstring

Your own identifier for this submitter — the primary identity key when you send it.

Prefer it over relying on owner alone. Matching on your id rather than on an email address is what makes one address belonging to several of your tenants resolve to several distinct people instead of collapsing into one, and it survives the person changing their email.

Send it and owner together: the id is how the submitter is matched, and the address is how they are notified, so owner is still required.

Adding it later is safe. If you have been submitting without it, the first submission that carries an id is matched by owner and the id is adopted onto that existing person — your submitters are upgraded, not duplicated. From then on the id is what identifies them.

At most 255 characters; longer returns 422 rather than being truncated, because a truncated id would silently resolve to the wrong person. Compared exactly, including case.

issuerstring

Which of your systems submitterId came from. Only meaningful alongside submitterId.

Send it when more than one of your systems posts to the same portal: user 42 in your billing app is not user 42 in your analytics app, and without an issuer they would resolve to one person. With one system you can omit it — it defaults to this portal.

The same value the browser/widget route carries as its token iss claim, so a submitter resolves to the same person whichever route their feedback arrives on.

At most 255 characters; longer returns 422.

submitterNamestring

The submitter's name, as your own system holds it.

Send it and the portal shows a person. Omit it and the only name Craft can produce is the local part of owner, so jane.doe@acme.com reads as "jane.doe" on every card and in every digest.

Refreshed on every submission that matches, not only on the first one — the same as owner. If the person is renamed in your system, the next submission carrying their submitterId updates the stored name, so you never have to reconcile it separately.

One string, not first/last: it is split on the last space for storage and joined back for display, so what you send is what the portal shows.

Requires pass-through identity on the portal, like submitterId, and requires owner — a name with nobody to attach it to is a 422 rather than a value quietly dropped.

At most 256 characters; longer returns 422 rather than being truncated.

sourceUrlstring

The page this feedback was submitted from — including its query string.

The query string is the point, not an incidental: a tenant, an account or a product area often sits in a URL parameter, and a PM reading the feedback item needs the link that reproduces what the submitter was looking at. Nothing is stripped, re-encoded or normalised.

In a single-page application send the URL in view at submit, not the one the page first loaded with — they are usually different and only the first is useful.

Must be an absolute http:// or https:// URL of at most 2048 characters. Anything else (a relative path, a javascript: or data: URL, surrounding whitespace, a NUL byte) returns 422 rather than being stored or quietly cleaned up.

Optional, and omitting it is the ordinary case for a backend integration that has no page.

attachmentIdsstring[]

Files to attach, as ids returned by POST /feedback_portal/{portalId}/attachment.

Upload first, then submit. The upload call stores the bytes against the portal and hands back an id; naming that id here is what files it with this piece of feedback. That order is what lets a form attach files while the submitter is still typing — there is no feedback item to attach to until they press send — and it means a failed upload costs a retry rather than the whole submission.

Each id must be one of this portal's own uploads and must not already be attached to another feedback item; anything else is a 422. At most 10 per item. Unlike metadata, an id that resolves to nothing is never quietly dropped — an attachment the submitter watched upload must not vanish behind a 200.

metadataobject

Context your own application wants to attach — the plan, the application version, the tenant — keyed by custom-field id and mapped onto this portal's own custom fields.

A key matching no field on the form is ignored, never a failure. That is the whole contract: your page's template ships on your release cycle and a PM changes the portal's fields whenever they like, so the two WILL drift, and a stale key must never cost you a piece of feedback. The same applies to a value the field cannot hold — dropped, not rejected. Use customFields when you want an unknown id or a bad value to be a 422 instead.

Keyed by id, not by display name, precisely because a name is what gets renamed. Discover ids via GET /feedback_portal/{portalId}/widget/{widgetId}/config or GET /feedback_portal/{portalId}/forms.

customFields wins. A field named in both is taken from customFields, because that is where the value the submitter could see and change arrives.

Never identity. The submitter's name, email and company come from the signed token (or from owner/company) and are never read from here — an unsigned value would be a second source of something Craft already holds signed. Since keys are custom-field ids, no key can name them.

At most 20 keys and 4 KB in total, and unlike an unknown key this is a 422: sending your whole application state is a bug you hit on the first try, not drift that appears months later.

Example request

{
  "owner": "user@example.com",
  "widgetId": "2305843010139571300",
  "importanceId": "Critical",
  "anonymous": true,
  "submitterId": "user_84213",
  "issuer": "billing-app",
  "submitterName": "Jane Doe",
  "sourceUrl": "https://app.acme.com/reports?tenant=acme-eu&view=board",
  "attachmentIds": [
    "2305843010342823571"
  ],
  "metadata": {
    "2305843010139571301": "Enterprise",
    "2305843010139571302": "4.18.2"
  }
}

Response

Create a plain feedback

string required

Changes

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      added the non-success response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status