Skip to main content

Implementing Pass-through Identity in Your Application - Developer Guide

A technical guide for developers: authenticate with a JWT or an account API key, identify submitters, submit feedback, and fill in custom fields using Craft.io's Pass-through Identity API.

Written by Zvi Cohen Zion

Learn how to integrate Craft.io’s Pass-through Identity API with your existing applications, forms, or systems to submit feedback while preserving the original submitter’s identity.

This guide covers authentication using signed JSON Web Tokens (JWTs) or an account API key, identifying submitters, submitting feedback, and populating custom fields.

For instructions on enabling Pass-through Identity and choosing between the available feedback collection approaches, see Collect Feedback Directly from Your Product and Existing Systems into Craft.io.
​


Before you begin

Before implementing Pass-through Identity, make sure you have:

  • Pass-through Identity enabled: An admin must generate an authentication secret key under Feedback Portal Settings → Feedback Widgets → Authentication.

  • Authentication credentials: Either the generated secret key for JWT authentication or an account API key, which you can request from your Craft.io Customer Success Manager.

  • A way to identify submitters: Your application or system should provide user information, such as a unique ID, name, or email address.

💡 Note: Your authentication method depends on whether you’re submitting feedback from your server or a browser. The next section explains the available options.


1. Authenticate your requests

Pass-through Identity supports two authentication methods: signed JWTs and account API keys. Choose the method that best fits your integration.

Route

Credential

Use it when

Signed token, from your server

A token you sign with the portal's secret key, sent as Authorization: Bearer

You want to stay self-serve. The secret key is already yours; there's nothing else to request.

Signed token, from a browser

The same token, fetched by the page from an endpoint on your server

The call is made from a page. An API key in a page can be read by anyone who opens it; a short-lived token can't be reused.

Account API key

x-api-key header

Your server makes the call and you also want to read the portal's categories, forms, or companies. See the limit below.

💡 What a signed token can't do: it submits feedback and uploads attachments, and that's all. Reading the portal's reference data — /categories, /importances, /forms, /custom_fields, and /companies — is refused with a 403 on a token. If your integration needs to read those, use an account API key, or look the values up once and keep them in your configuration.

To get an account API key, contact your Customer Success Manager (CSM).

Signing your own token

If you're using the secret key route, the token is an ordinary JWT, signed HS256 with the portal's secret key. It must include:

Claim

Required

Description

sub

Yes

Your own ID for this person, as a string. This is the primary identity key.

email

Yes

Their email address.

iat

Yes

Token creation timestamp, in Unix seconds.

exp

Yes

Token expiration timestamp, in Unix seconds. Must be no more than one hour after iat.

name

No

Their name as your system holds it.

company

No

The company the person belongs to, written exactly as it appears in your portal's company list.

Identity then comes from the token's claims only: sending submitterId, owner, submitterName, issuer, or anonymous in the request body alongside a token is refused.

💡 Need working code? The Feedback Widget Developer Guide has a complete Node.js example of signing this same kind of token. Nothing about the signing process is widget-specific — the claims above are everything Craft.io requires.


2. Identify the submitter

When using an account API key, include the submitter’s identity in the JSON request body, alongside the feedback title and description.
​

When using a signed JWT, the submitter’s identity is taken from the token’s claims instead. In this case, identity fields must be omitted from the request body.

The following example shows how to identify a submitter when using an account API key.
​

{
"title": "Bulk import fails over 10k rows",
"description": "Stops at about 10,000 and reports no error.",

"submitterId": "crm-5541",
"submitterName": "Dana Whitfield",
"owner": "dana@northwind.test",
"issuer": "northwind-crm"
}

Field

What to send

submitterId

Your own ID for this person. This is the primary identity key: send it and Craft.io matches on it rather than on the email address.

owner

Their email address. Used for status-update emails, and as the identity key when you send no submitterId.

submitterName

Their name as your system holds it. Omit it and the portal can only show the local part of the email where a person's name should appear.

issuer

Which of your systems the ID came from. Worth sending if more than one system posts to the same portal, so two different people with the same ID don't collide.

company

The company the person belongs to, written exactly as it appears in your portal's company list. A name matching nothing is refused rather than created. Accepted on restricted portals; a public portal files everything under its default company.

anonymous

Send true to deliberately attribute nothing. The feedback is filed under the portal's own Anonymous user. This can't be combined with submitterId or owner; sending it alongside either is refused.

💡 Use a consistent submitter ID. Craft.io uses submitterId to recognize the same person across submissions, even if their name or email address changes. This helps prevent duplicate submitter records.


3. Submit the feedback

Post to your portal's submission endpoint:

POST https://{api-host}/feedback_portal/{portalId}/plain_feedback

Use /plain_feedback for standard feedback submissions, or /feedback when submitting custom field values. See Section 4 for details.

Which host

Your account is hosted in

Use

US

https://api.craft.io

EU

https://api-eu.craft.io

If you're not sure which you're on, check the address you use for Craft.io itself, or ask your CSM. The examples below use the US host.

Values you'll need

Value

Where to get it

portalId

Portal settings → Feedback Widgets → Installation, where it appears as data-portal. With an account API key you can also read it from GET /feedback_portals. Otherwise ask your CSM; it doesn't change, so you only need it once.

categoryId

Optional; leave it out and the feedback lands in the portal's default category. To choose one, the IDs come from GET /feedback_portal/{portalId}/categories, which needs an account API key.

importanceId

Optional, and it takes the name: send "Critical" rather than an id. Omit it and the portal's default is used.

company

The company name, exactly as your portal lists it. No id needed.

💡 Start with the essentials. The feedback title is the only required content field. Category, importance, and company are optional and use the portal’s defaults when omitted. To attribute feedback to a specific person, include the appropriate identity information for your chosen authentication method.

Follow these steps:

  1. Build the request body with your title, description, and the identity fields from the previous section.

  2. Send it to the endpoint above with your chosen credential.

curl --request POST \
"https://api.craft.io/feedback_portal/$PORTAL_ID/plain_feedback" \
-H "x-api-key: $CRAFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Bulk import fails over 10k rows",
"description": "Stops at about 10,000 and reports no error.",
"categoryId": "2305843010139563682",
"submitterId": "crm-5541",
"submitterName": "Dana Whitfield",
"owner": "dana@northwind.test",
"issuer": "northwind-crm"
}'

With a signed token, drop the identity fields from the body and send the token instead:

curl --request POST \
"https://api.craft.io/feedback_portal/$PORTAL_ID/plain_feedback" \
-H "Authorization: Bearer $SIGNED_JWT" \
-H "Content-Type: application/json" \
-d '{
"title": "Bulk import fails over 10k rows",
"description": "Stops at about 10,000 and reports no error.",
"categoryId": "2305843010139563682"
}'

A successful response returns the new item's id, its shortId, and a link to it in the portal.

💡 Using custom fields? To submit additional information such as product area, browser version, or severity, follow the instructions in Section 4.


4. Fill in your form's custom fields

Each feedback category in your portal is associated with a form, which may include custom fields in addition to the standard title and description.

To submit values for these fields, use the /feedback endpoint instead of /plain_feedback and include a customFields array in your request body.

POST https://{api-host}/feedback_portal/{portalId}/feedback

💡 Note: /plain_feedback refuses custom fields. Sending customFields there comes back as a 422; they aren't silently dropped.

Find out which fields to send

Ask the portal for its forms, with the choices for each select field included:

curl "https://api.craft.io/feedback_portal/$PORTAL_ID/forms?includeOptions=true" \
-H "x-api-key: $CRAFT_API_KEY"

Each form lists the categories that use it and its fields, in the order the form shows them:

{
"metadata": { "portalId": "2305843010139563650", "count": 2 },
"forms": [
{
"id": "2305843010139563676",
"name": "Bug report",
"isDefault": false,
"categories": [ { "id": "2305843010139563682", "name": "Bugs" } ],
"fields": [
{
"id": "2305843010139571301",
"name": "Product area",
"type": "SELECT",
"multi": false,
"required": true,
"options": [
{ "id": "2305843010139571310", "label": "Registration" },
{ "id": "2305843010139571311", "label": "Mobile app" }
],
"source": "portal",
"orderIndex": 1
},
{
"id": "2305843010139571302",
"name": "App version",
"type": "TEXT",
"required": false,
"source": "portal",
"orderIndex": 2
}
]
},
{ "id": "...", "name": "Default Form", "isDefault": true, "categories": [], "fields": [] }
]
}

To pick the form for your feedback:

  1. Find the form whose categories include the category you're submitting to.

  2. If no form lists it, or you send no categoryId and the default category has no form of its own, use the form with "isDefault": true. That's the form Craft.io checks your fields against.
    ​

💡 Read this once, not on every submission. Field and option IDs remain the same even when renamed. Retrieve them when setting up your integration, store them in your configuration, and refresh them when fields or options are added or removed.

💡 An account API key is required to retrieve form details. Signed JWTs can submit feedback with custom fields but cannot retrieve form definitions through /forms (403 Forbidden).

If you’re using JWT authentication, retrieve the form and field IDs once using an account API key, then store them in your integration’s configuration.

Write each value

customFields is a list. Each entry names one field by its id from the form, and carries either value (one value) or values (several):

Field type

What to send

Example

TEXT, LONG_TEXT

A string, up to 65,535 characters

{"id": "...", "value": "4.18.2"}

NUMBER

A number. The field's minimum and maximum, if set, are enforced.

{"id": "...", "value": 42}

DATE

A date as YYYY-MM-DD

{"id": "...", "value": "2026-10-13"}

LINK

A full https://, http://, or mailto: address

{"id": "...", "value": "https://..."}

SELECT, single choice

One option, by its id or its label, in value

{"id": "...", "value": "Registration"}

SELECT, multiple choice

One or more options, by id or label, in values

{"id": "...", "values": ["Registration", "2305843010139571311"]}

USER

The email address of someone on your Craft.io team

{"id": "...", "value": "pm@yourcompany.com"}

FORMULA

Nothing. It's calculated, and any value is refused.

—

💡 Note: An option's id is the safer choice: it survives a PM renaming the option, and a label that matches nothing is refused rather than created.

The full request

curl --request POST \
"https://api.craft.io/feedback_portal/$PORTAL_ID/feedback" \
-H "x-api-key: $CRAFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Registration page times out for large events",
"description": "Over 2,000 attendees and the page never finishes loading.",
"categoryId": "2305843010139563682",
"submitterId": "crm-5541",
"submitterName": "Dana Whitfield",
"owner": "dana@northwind.test",
"issuer": "northwind-crm",
"sourceUrl": "https://app.northwind.test/acme/events/812/registration",
"customFields": [
{ "id": "2305843010139571301", "value": "2305843010139571310" },
{ "id": "2305843010139571302", "value": "4.18.2" }
]
}'

Here, the first customFields entry picks Registration for Product area by its option id, and the second fills in App version. sourceUrl is optional: it records the page the person was on, and it appears on the item in Craft.io.

With a signed token, the body is the same, minus the identity fields.

Rules the API checks

  • Every field must be on the form you picked above. A field from another category's form, or an id the portal doesn't have, is a 422.

  • Only fields with "source": "portal" can be set when you create feedback. These are the fields on your portal's forms.

  • Send each field once. The same id twice in one request is refused.

  • Send a value for every field the form marks "required": true. Always provide a value for every required field — a required field sent with an empty value is refused.

  • A single-choice field takes one option. Sending several values to a field with "multi": false is refused.
    ​


How submitters are represented in Craft.io

When feedback is submitted on behalf of a person for the first time, Craft.io automatically creates a portal user for them.

These users don’t need a password or a Craft.io account. Their identity is maintained so your team can associate feedback with the correct person and send relevant status updates.

If email notifications are enabled, submitters can receive updates at the email address you provided and unsubscribe at any time.


Security considerations

  • Keep your secret key on the server. Never expose it in frontend JavaScript, HTML, or any publicly accessible configuration — anyone with it can sign tokens as any of your users.

  • Use short-lived tokens. The maximum supported validity period is one hour; a shorter expiration, such as 15 minutes, limits how long a leaked token stays useful.

  • Don't cache tokens. Generate a fresh one per session and avoid storing them in logs or client-side storage.

  • Authenticate your own token-issuing endpoint. If you expose an endpoint that signs tokens for a browser page, make sure only your own authenticated users can call it.

  • Rotate or revoke the secret key if it's compromised. Rotating under Feedback Portal Settings → Feedback Widgets → Authentication keeps the previous key valid for seven days; revoking invalidates it immediately. Either action affects every integration using that key, including the Feedback Widget if you also use it.

  • Treat the account API key with the same care as the secret key. It can read portal data as well as submit feedback, so restrict it to your server-side code.
    ​


Troubleshooting

What you get

What it usually is

422 naming a new person

Pass-through Identity isn't enabled on the portal. An admin needs to generate a secret key; see Collect Feedback Directly from Your Product and Existing Systems into Craft.io.

422 on customFields

Sent to /plain_feedback, which accepts none; use /feedback. Otherwise the field isn't on the category's form, the option matches nothing, or a single-choice field got several values. See section 4.

403 reading /forms

You're using a signed token. Reading forms needs an account API key.

422 on company

The name matches no company in your portal, or the portal is public. Check the portal's company list.

403

The API key can't write feedback on this portal. Check its scopes and portal access with your CSM.

422 listing identity fields

You sent identity in the body alongside a signed token. With a token, identity comes from the token only.

Feedback shows as Anonymous

No submitterId and no owner was sent, or anonymous: true was.

One person became two

Matching fell back to the email address and it changed. Send submitterId and it won't happen again.

If the issue persists, contact Craft.io Support.


What comes next

Need more guidance? 🙋 Our LIVE support team (at the bottom right corner of your screen) replies to ANY question!

Did this answer your question?