Upgrading from v1.0
This guide covers what Farcaster clients must change when upgrading from snap spec v1.0 to v2.0. Both the POST payload format and the fallback behavior have changed.
POST Payload Changes
New required fields: audience, user, and surface
v2 POST payloads must include audience, user, and surface:
| Field | Type | Description |
|---|---|---|
audience | string | Origin of the target snap server (scheme://host) |
user | { fid: number } | User taking the action |
surface | discriminated union | Where the interaction happens — see Surfaces |
audience prevents a signed payload meant for one snap from being replayed against
a different server. Set it to new URL(snapUrl).origin.
The top-level fid field is deprecated in favor of user.fid but MUST still be
included with the same value until older integrations no longer depend on it.
Removed: button_index
The button_index field is no longer included in v2 POST payloads. Snap servers now use
distinct submit target URLs to identify which button was pressed. Clients should set
the POST URL from the button's on.press.params.target.
v2 payload example
// When embedded in a cast, set castContext to { hash, authorFid }; otherwise undefined.
declare const castContext: { hash: string; authorFid: number } | undefined;
const payload = {
fid: user.fid,
user: { fid: user.fid },
inputs: collectInputValues(),
timestamp: Math.floor(Date.now() / 1000),
audience: new URL(targetUrl).origin,
surface: castContext
? {
type: "cast",
cast: {
hash: castContext.hash,
author: { fid: castContext.authorFid },
},
}
: { type: "standalone" },
};
Fallback to v1 on Failure
Not all snap servers have upgraded to v2 yet. Clients must handle the case where a v2 POST fails by falling back to the v1 payload format.
The recommended flow:
- Send the v2 payload (with
audience,user, andsurface, withoutbutton_index) - If the server returns an error (4xx), retry with a v1 payload (with
button_index, withoutaudience/user/surface) - If the retry also fails, display the error to the user
async function submitAction(
targetUrl: string,
user: User,
inputs: Record<string, unknown>,
buttonIndex: number,
castContext?: { hash: string; authorFid: number },
) {
// Try v2 first
const v2Payload = {
fid: user.fid,
user: { fid: user.fid },
inputs,
timestamp: Math.floor(Date.now() / 1000),
audience: new URL(targetUrl).origin,
surface: castContext
? {
type: "cast" as const,
cast: {
hash: castContext.hash,
author: { fid: castContext.authorFid },
},
}
: { type: "standalone" as const },
};
let response = await sendSignedPost(targetUrl, v2Payload);
if (!response.ok) {
// Fall back to v1
const v1Payload = {
fid: user.fid,
inputs,
timestamp: Math.floor(Date.now() / 1000),
button_index: buttonIndex,
};
response = await sendSignedPost(targetUrl, v1Payload);
}
if (!response.ok) {
throw new Error(`Snap server error: ${response.status}`);
}
return response.json();
}
This fallback ensures clients work with both old (v1) and new (v2) snap servers during the transition period.
Structural Constraints
v2 snaps enforce structural limits on the UI tree. Clients should expect all v2 snaps to fit within these bounds:
| Constraint | Limit |
|---|---|
| Total elements | 64 |
| Root children | 7 |
| Children per container | 6 |
| Nesting depth | 4 levels |
| Height | 500px (clip content beyond this) |
Checklist
- Send
audience,user: { fid }, andsurfacein every v2 POST (keep top-levelfidequal touser.fiduntil deprecated field is removed) - Use
surface.type: "cast"withcast.hashandcast.authorwhen in cast context; otherwisesurface.type: "standalone" - Remove
button_indexfrom v2 payloads - Implement v1 fallback — retry with
button_indexand without v2-only fields if the v2 POST fails - Clip snap rendering at 500px height
- Use the button's
targetURL as the POST destination