> ## Documentation Index
> Fetch the complete documentation index at: https://widgetic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Make a secret to sign the context of your embeds

> Makes a secret to sign the `context` of your embeds with, and returns it in `secret`: **keep it now, it is
shown once** and cannot be read again. Keep it on your server only: whoever holds it can write contexts your
compositions accept. Call this again to make a new secret; the one it replaces is still accepted for a day, so
that your server can switch without an entry being refused (making another within that day ends the overlap
with the first).

**What signing is for.** `<widgetic composition-id="..." context='{"customerId":"c_42"}'>` hands attribution to
a widget, which sends it with every entry it saves. The page is yours but the browser is the visitor's:
anybody can change that attribute and file an entry under another customer's id. When your server signs the
context, the gateway refuses an entry whose context was changed, and the entries you keep hold attribution
your server wrote.

**How to sign** (on your server, for each page you render):

1. Take the context to give the widget. It may hold text and whole numbers only (within ±2^53, written without
   a fraction or an exponent).
2. Choose when the signature stops being good: `widgetic_exp`, in seconds since 1970 (UTC), no more than
   seven days from now. A minute of difference between clocks is forgiven.
3. Write the text to sign, with no spaces except those inside values:
   `widgetic-context.v1.<compositionId>.<widgetic_exp>.<json>`. `<compositionId>` is the id of the composition
   in lower case. `<json>` is the context as JSON with the keys in character-code order (so `Z` comes before
   `a`, and `10` before `9`), no spaces, strings escaped as JSON escapes them, and the two keys of the
   signature left out.
4. Sign that text: HMAC-SHA256 with the 32 bytes the secret stands for (the base64 text after `ctxsec_`), and
   write the result as base64url without padding.
5. Give the widget the context with two keys added: `widgetic_exp` and `widgetic_sig` (the signature). The whole
   attribute, the two keys included, stays within the limits of any context: at most 20 keys, 2048 bytes of
   JSON, strings up to 500 characters. The two keys take two of the 20 keys and 87 bytes of the 2048.

A test vector, to check your code against before you rely on it. Secret
`ctxsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=` (the bytes 0 to 31), composition
`7c9e6679-7425-40de-944b-e07fc1f90ae7`, `widgetic_exp` 1792000000 and context
`{"customerId":"c_42","plan":"pro","seats":3}` sign the text
`widgetic-context.v1.7c9e6679-7425-40de-944b-e07fc1f90ae7.1792000000.{"customerId":"c_42","plan":"pro","seats":3}`
and give the signature `j6g7151F54tl9C8oEY9levaYSppHME4fAPZ7WPwL8C8`. (The secret is the example's, not a
real one.)

**What the gateway does with an entry.** When you have a secret, an entry whose context carries a signature is
checked: a signature that does not match is refused with 403 `CONTEXT_SIGNATURE_INVALID`, and one that is past
`widgetic_exp` with 403 `CONTEXT_SIGNATURE_EXPIRED`. When you also require signatures
(`updatePartnerEntryContextSigning`) an entry whose context carries none is refused with 403
`CONTEXT_SIGNATURE_REQUIRED`. The two keys are taken out before the entry is stored or announced: your entries
and your `entry.created` events hold the context you wrote. A change you make reaches every part of the
gateway within a minute.

**What it does not do.** A signed context that has been seen (a page's source is public) can be sent again by
anyone until `widgetic_exp`; when you must not accept the same context twice, put a value that is new for each
page view in it (a `submissionId` you made up) and keep the ones you have seen. The signature covers the
entries that reach Widgetic's gateway: a row that gets into a table another way (your own code writing
straight into a database you brought, or the record calls of the composition's owner) does not pass
through it, and carries no context.

Retry this call with the same `Idempotency-Key` if its answer was lost: the answer, secret included, is then
given again, and no second secret is made. This needs an API key with full access. The caller must be the
person who administers the partner, or a Widgetic administrator.




## OpenAPI

````yaml /openapi/widgetic-api-public.json post /partners/{partnerId}/entry-context-signing/secret
openapi: 3.0.0
info:
  title: Widgetic API Gateway
  version: 1.0.0
  description: API documentation for the Widgetic API Gateway
  license:
    name: Widgetic Integration License
    url: https://docs.widgetic.com/resources/license
servers:
  - url: https://api.widgetic.com/v1
    description: Production server
    variables:
      env:
        default: production
        description: Environment
      apiUrl:
        default: https://api.widgetic.com
        description: API URL
      apiVersion:
        default: /v1
        description: API Version path prefix
security: []
tags: []
paths:
  /partners/{partnerId}/entry-context-signing/secret:
    post:
      tags:
        - Partners
      summary: Make a secret to sign the context of your embeds
      description: >
        Makes a secret to sign the `context` of your embeds with, and returns it
        in `secret`: **keep it now, it is

        shown once** and cannot be read again. Keep it on your server only:
        whoever holds it can write contexts your

        compositions accept. Call this again to make a new secret; the one it
        replaces is still accepted for a day, so

        that your server can switch without an entry being refused (making
        another within that day ends the overlap

        with the first).


        **What signing is for.** `<widgetic composition-id="..."
        context='{"customerId":"c_42"}'>` hands attribution to

        a widget, which sends it with every entry it saves. The page is yours
        but the browser is the visitor's:

        anybody can change that attribute and file an entry under another
        customer's id. When your server signs the

        context, the gateway refuses an entry whose context was changed, and the
        entries you keep hold attribution

        your server wrote.


        **How to sign** (on your server, for each page you render):


        1. Take the context to give the widget. It may hold text and whole
        numbers only (within ±2^53, written without
           a fraction or an exponent).
        2. Choose when the signature stops being good: `widgetic_exp`, in
        seconds since 1970 (UTC), no more than
           seven days from now. A minute of difference between clocks is forgiven.
        3. Write the text to sign, with no spaces except those inside values:
           `widgetic-context.v1.<compositionId>.<widgetic_exp>.<json>`. `<compositionId>` is the id of the composition
           in lower case. `<json>` is the context as JSON with the keys in character-code order (so `Z` comes before
           `a`, and `10` before `9`), no spaces, strings escaped as JSON escapes them, and the two keys of the
           signature left out.
        4. Sign that text: HMAC-SHA256 with the 32 bytes the secret stands for
        (the base64 text after `ctxsec_`), and
           write the result as base64url without padding.
        5. Give the widget the context with two keys added: `widgetic_exp` and
        `widgetic_sig` (the signature). The whole
           attribute, the two keys included, stays within the limits of any context: at most 20 keys, 2048 bytes of
           JSON, strings up to 500 characters. The two keys take two of the 20 keys and 87 bytes of the 2048.

        A test vector, to check your code against before you rely on it. Secret

        `ctxsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=` (the bytes 0 to
        31), composition

        `7c9e6679-7425-40de-944b-e07fc1f90ae7`, `widgetic_exp` 1792000000 and
        context

        `{"customerId":"c_42","plan":"pro","seats":3}` sign the text

        `widgetic-context.v1.7c9e6679-7425-40de-944b-e07fc1f90ae7.1792000000.{"customerId":"c_42","plan":"pro","seats":3}`

        and give the signature `j6g7151F54tl9C8oEY9levaYSppHME4fAPZ7WPwL8C8`.
        (The secret is the example's, not a

        real one.)


        **What the gateway does with an entry.** When you have a secret, an
        entry whose context carries a signature is

        checked: a signature that does not match is refused with 403
        `CONTEXT_SIGNATURE_INVALID`, and one that is past

        `widgetic_exp` with 403 `CONTEXT_SIGNATURE_EXPIRED`. When you also
        require signatures

        (`updatePartnerEntryContextSigning`) an entry whose context carries none
        is refused with 403

        `CONTEXT_SIGNATURE_REQUIRED`. The two keys are taken out before the
        entry is stored or announced: your entries

        and your `entry.created` events hold the context you wrote. A change you
        make reaches every part of the

        gateway within a minute.


        **What it does not do.** A signed context that has been seen (a page's
        source is public) can be sent again by

        anyone until `widgetic_exp`; when you must not accept the same context
        twice, put a value that is new for each

        page view in it (a `submissionId` you made up) and keep the ones you
        have seen. The signature covers the

        entries that reach Widgetic's gateway: a row that gets into a table
        another way (your own code writing

        straight into a database you brought, or the record calls of the
        composition's owner) does not pass

        through it, and carries no context.


        Retry this call with the same `Idempotency-Key` if its answer was lost:
        the answer, secret included, is then

        given again, and no second secret is made. This needs an API key with
        full access. The caller must be the

        person who administers the partner, or a Widgetic administrator.
      operationId: rotatePartnerEntryContextSecret
      parameters:
        - in: path
          name: partnerId
          required: true
          schema:
            type: string
            format: uuid
          description: The partner whose secret to make
        - $ref: '#/components/parameters/IdempotencyKeyRequired'
      responses:
        '200':
          description: The new secret, and how your signing stands now
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerEntryContextSecretResponse'
        '400':
          description: The `Idempotency-Key` header is missing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: The API key is missing or invalid
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: >-
            The caller does not administer this partner, or the API key has not
            full access
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: There is no such partner (reported to administrators only)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: >-
            Too many changes in a short time. Try again after the Retry-After
            seconds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: >-
            Signed context is not available on this server right now
            (ENTRY_CONTEXT_SIGNING_UNAVAILABLE). Nothing was changed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - apiKeyAuth: []
      x-code-samples:
        - lang: TypeScript
          label: TypeScript SDK
          source: >
            import { randomUUID } from 'node:crypto';

            import { Configuration, PartnersApi } from '@widgetic/api-sdk-ts';


            const partnersApi = new PartnersApi(new Configuration({ apiKey:
            'YOUR_API_KEY' }));


            const { data: made } = await
            partnersApi.rotatePartnerEntryContextSecret({ partnerId,
            idempotencyKey: randomUUID() });


            // Shown once: keep it on your server now.

            console.log(made.secret);
        - lang: TypeScript
          label: Sign a context (Node.js)
          source: >
            import { createHmac } from 'node:crypto';


            // Returns the context to put in the `context` attribute: yours,
            plus widgetic_exp and widgetic_sig.

            function signContext(
              secret: string, // the ctxsec_ text you were shown
              compositionId: string,
              context: Record<string, string | number>, // text and whole numbers only
              lifetimeSeconds = 3600,
            ) {
              const key = Buffer.from(secret.slice('ctxsec_'.length), 'base64');
              const widgeticExp = Math.floor(Date.now() / 1000) + lifetimeSeconds;
              const json =
                '{' +
                Object.keys(context)
                  .sort()
                  .map((name) => `${JSON.stringify(name)}:${JSON.stringify(context[name])}`)
                  .join(',') +
                '}';
              const text = `widgetic-context.v1.${compositionId.toLowerCase()}.${widgeticExp}.${json}`;
              const widgeticSig = createHmac('sha256', key).update(text).digest('base64url');
              return { ...context, widgetic_exp: widgeticExp, widgetic_sig: widgeticSig };
            }


            // <widgetic composition-id="..."
            context='${JSON.stringify(signContext(secret, id, { customerId:
            'c_42' }))}'>
components:
  parameters:
    IdempotencyKeyRequired:
      in: header
      name: Idempotency-Key
      required: true
      description: >-
        Required: a call without it is refused with `400`. A key of your own
        that names this one request; a UUID is ideal. It is 8 to 50 characters
        of letters, digits, `-` and `_`. Send the same key again when you retry
        (after a timeout, a dropped connection or a `5xx`) and the call is
        carried out once: the retry gets the first answer back, marked
        `X-Idempotency-Cached: true`. The same key with a different body is
        refused with `422`, and a retry that arrives while the first call is
        still running with `409` (see `Retry-After`).
      schema:
        type: string
        minLength: 8
        maxLength: 50
        pattern: ^[a-zA-Z0-9_-]{8,50}$
        example: 9b2d6f0a-3c41-4e5a-8d77-1f2e3a4b5c6d
  schemas:
    PartnerEntryContextSecretResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          description: Always `true` on a success.
          example: true
        data:
          $ref: '#/components/schemas/PartnerEntryContextSecret'
        meta:
          $ref: '#/components/schemas/ResponseMeta'
    ErrorResponse:
      type: object
      required:
        - success
        - message
        - error
      additionalProperties: true
      properties:
        success:
          type: boolean
          description: Always `false` on a failure.
          example: false
        message:
          type: string
          description: The same text as `error.message`, for a caller that reads one line.
        error:
          $ref: '#/components/schemas/ErrorDetail'
    PartnerEntryContextSecret:
      type: object
      description: A new signing secret, and how your signing stands now.
      required:
        - signing
        - secret
      properties:
        signing:
          $ref: '#/components/schemas/PartnerEntryContextSigning'
        secret:
          type: string
          description: >-
            The secret: `ctxsec_` followed by the base64 of 32 random bytes. It
            is shown here, once, and cannot be read again.
      example:
        signing: null
        secret: Example secret
    ResponseMeta:
      type: object
      description: >-
        Extra information about the answer: where a page sits in a list, the
        cursor of the next page, or warnings about a call that succeeded with a
        caveat.
      additionalProperties: true
      properties:
        page:
          type: integer
          description: The page that was read, counting from 1.
          example: 1
        size:
          type: integer
          description: How many items one page holds.
          example: 20
        total:
          type: integer
          description: How many items the whole list has.
          example: 135
        limit:
          type: integer
          description: The same as `size`.
          example: 20
        offset:
          type: integer
          description: How many items come before this page.
          example: 0
        hasNextPage:
          type: boolean
          example: true
        hasPreviousPage:
          type: boolean
          example: false
        nextCursor:
          type: string
          nullable: true
          description: >-
            Pass this as `cursor` to read the next page of a list that is read
            by cursor. `null` on the last page.
        warnings:
          type: array
          items:
            type: string
          description: >-
            Things the call did not do, though it succeeded (for example a table
            that could not be created just now).
    ErrorDetail:
      type: object
      required:
        - code
        - message
        - timestamp
      properties:
        code:
          type: string
          description: Machine-readable error code.
          example: RESOURCE_NOT_FOUND
        message:
          type: string
          description: Human-readable explanation.
          example: Composition not found
        param:
          type: string
          description: The field that caused the error, when one did.
          example: name
        docUrl:
          type: string
          description: Where this error is explained.
        suggestedAction:
          type: string
          description: What to do about it.
        retryAfter:
          type: integer
          description: >-
            Seconds to wait before trying again (rate limits). The same number
            is in the `Retry-After` header.
          example: 60
        details:
          type: object
          additionalProperties: true
          description: >-
            More about the error: the fields that failed, a limit that was
            reached.
        correlationId:
          type: string
          description: Quote this when you ask support about the call.
          example: req_123e4567-e89b-12d3-a456-426614174000
        timestamp:
          type: string
          format: date-time
          example: '2026-10-15T10:00:00.000Z'
    PartnerEntryContextSigning:
      type: object
      description: >-
        How the context of your embeds is signed. The secret that signs it is
        never part of this: it is shown once, when it is made.
      required:
        - partnerId
        - required
        - secretSet
        - secretRotatedAt
        - previousSecretValidUntil
        - signingAvailable
      properties:
        partnerId:
          type: string
          format: uuid
        required:
          type: boolean
          description: >-
            True: an entry whose context carries no valid signature is refused
            (403 `CONTEXT_SIGNATURE_REQUIRED`). False: an entry that carries a
            signature is checked, and one that carries none is accepted as
            before.
        secretSet:
          type: boolean
          description: >-
            Whether you have a signing secret. Until you have, no signature is
            checked and none can be required.
        secretRotatedAt:
          type: string
          format: date-time
          nullable: true
          description: When the current secret was made. Null while there is none.
        previousSecretValidUntil:
          type: string
          format: date-time
          nullable: true
          description: >-
            While you switch to a new secret, when the secret it replaced stops
            being accepted. Null when there is none to wait for.
        signingAvailable:
          type: boolean
          description: >-
            False when this server cannot keep secrets, in which case none can
            be made yet.
      example:
        partnerId: 3fa85f64-5717-4562-b3fc-2c963f66afa6
        required: true
        secretSet: true
        secretRotatedAt: '2026-01-15T09:30:00.000Z'
        previousSecretValidUntil: '2026-01-15T09:30:00.000Z'
        signingAvailable: true
  securitySchemes:
    apiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.