openapi: '3.0.3'
info:
  title: GSD API
  version: 1.0.0
  description: |
    Send documents for signature from your own code: create envelopes from
    templates you built in the GSD editor, send them, track status, receive
    signed webhooks, and download the signed PDF plus the certificate of
    completion.

    Human documentation, quickstart and webhook guide: https://gsdhere.com/developers

    Every request authenticates with a bearer API key (created in the app
    under Settings → API & Webhooks; keys look like gsd_live_A1b2C3d4…).
    All errors share one envelope (see the Error schema). Rate limits:
    120 requests/minute per key across everything, plus a separate
    30 requests/minute per key for the two POST endpoints. POSTs accept an
    Idempotency-Key header — same key + same body within 24 hours replays the
    stored response; same key + different body returns 409.
  contact:
    email: hello@gsdhere.com
servers:
  - url: https://gsdhere.com
security:
  - bearerAuth: []
tags:
  - name: Templates
  - name: Envelopes
  - name: Documents
paths:
  /api/v1/templates:
    get:
      tags: [Templates]
      summary: List your active templates
      description: >
        Newest first, with the signer roles you need when creating envelopes.
        Cursor pagination: pass the previous page's next_after as after.
      operationId: listTemplates
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/After'
      responses:
        '200':
          description: Your active templates.
          content:
            application/json:
              schema:
                type: object
                required: [ok, templates, has_more, next_after]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  templates:
                    type: array
                    items:
                      $ref: '#/components/schemas/Template'
                  has_more:
                    type: boolean
                  next_after:
                    type: integer
                    nullable: true
                    description: Pass as ?after= to fetch the next page (null on the last page).
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/envelopes:
    get:
      tags: [Envelopes]
      summary: List envelopes
      description: 'Newest first. Filter by status; cursor pagination via after.'
      operationId: listEnvelopes
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [draft, sent, in_progress, completed, voided, expired]
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/After'
      responses:
        '200':
          description: Envelopes in your workspace.
          content:
            application/json:
              schema:
                type: object
                required: [ok, envelopes, has_more, next_after]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  envelopes:
                    type: array
                    items:
                      $ref: '#/components/schemas/EnvelopeSummary'
                  has_more:
                    type: boolean
                  next_after:
                    type: integer
                    nullable: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags: [Envelopes]
      summary: Create an envelope from a template (optionally send it)
      description: >
        Provide exactly one signer per template role. With send true the
        envelope is sent immediately; otherwise it stays a draft you can send
        later with POST /api/v1/envelopes/{id}/send. Counts against the
        30/minute write budget.
      operationId: createEnvelope
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEnvelopeRequest'
      responses:
        '201':
          description: The envelope was created (and sent, if send was true).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateEnvelopeResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: 'paygo_cap: the free-envelope allowance is used up and the credit balance does not cover this send.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: 'invite_delivery_failed: every invite email failed; the envelope was returned to draft.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/envelopes/{id}:
    get:
      tags: [Envelopes]
      summary: Get an envelope — status, signers, payment
      operationId: getEnvelope
      parameters:
        - $ref: '#/components/parameters/EnvelopeId'
      responses:
        '200':
          description: The envelope with its signers.
          content:
            application/json:
              schema:
                type: object
                required: [ok, envelope]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  envelope:
                    $ref: '#/components/schemas/EnvelopeDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/envelopes/{id}/send:
    post:
      tags: [Envelopes]
      summary: Send a draft envelope
      description: >
        Emails every signer their signing link. Only drafts can be sent — a
        non-draft answers 409 envelope_not_draft. Counts against the 30/minute
        write budget.
      operationId: sendEnvelope
      parameters:
        - $ref: '#/components/parameters/EnvelopeId'
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The envelope went out.
          content:
            application/json:
              schema:
                type: object
                required: [ok, envelope, sent, failed]
                properties:
                  ok:
                    type: boolean
                    enum: [true]
                  envelope:
                    type: object
                    required: [id, status]
                    properties:
                      id:
                        type: integer
                      status:
                        type: string
                        enum: [sent]
                  sent:
                    type: integer
                    description: How many signer invitations were delivered.
                  failed:
                    type: array
                    description: Signers whose invitation email failed (partial failure — the envelope is still sent).
                    items:
                      type: object
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: 'paygo_cap: the free-envelope allowance is used up and the credit balance does not cover this send.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          description: 'invite_delivery_failed: every invite email failed; the envelope was returned to draft.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/envelopes/{id}/documents/signed:
    get:
      tags: [Documents]
      summary: Download the signed PDF
      description: >
        Available once the envelope status is completed; before that (or after
        files are removed under your plan's retention) the endpoint answers
        404 not_found.
      operationId: downloadSignedDocument
      parameters:
        - $ref: '#/components/parameters/EnvelopeId'
      responses:
        '200':
          description: The signed document.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/envelopes/{id}/certificate:
    get:
      tags: [Documents]
      summary: Download the certificate of completion (PDF)
      description: >
        The certificate records how each signer was authenticated, their
        consent record and per-step timestamps; envelopes created through the
        API note their API origination. Available once the envelope is
        completed.
      operationId: downloadCertificate
      parameters:
        - $ref: '#/components/parameters/EnvelopeId'
      responses:
        '200':
          description: The certificate of completion.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Your API key, e.g. Authorization: Bearer gsd_live_A1b2C3d4…'
  parameters:
    EnvelopeId:
      name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      description: Envelope id. Ids are per-workspace — a foreign id answers 404.
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: Page size.
    After:
      name: after
      in: query
      required: false
      schema:
        type: integer
      description: 'Cursor: the next_after value from the previous page.'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: >
        Any unique string per logical request (a UUID is perfect). Same key +
        same body within 24 hours replays the stored response (flagged with an
        Idempotency-Replayed: true header); same key + different body answers
        409 idempotency_conflict. Responses of 500 and above are never stored,
        so retries after a server error re-execute.
  responses:
    BadRequest:
      description: 'The request is malformed or fails validation (e.g. invalid_request, invalid_signer_email, invalid_payment_amount).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: 'invalid_api_key: the key is missing, malformed or revoked.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: 'plan_required, insufficient_scope or payment_collection_not_enabled.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: 'not_found / template_not_found: the id does not exist in your workspace (ids are per-workspace).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: 'envelope_not_draft, idempotency_conflict or email_not_configured.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: >
        rate_limited: over the per-key budget (120/minute overall, 30/minute
        for the write endpoints). Wait Retry-After seconds and retry.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      description: Every error uses this envelope.
      required: [ok, error]
      properties:
        ok:
          type: boolean
          enum: [false]
        error:
          type: object
          required: [code, message, fix, docs]
          properties:
            code:
              type: string
              description: Machine-readable error code.
              enum:
                - invalid_request
                - invalid_api_key
                - plan_required
                - insufficient_scope
                - rate_limited
                - not_found
                - template_not_found
                - template_has_no_fields
                - invalid_signer_email
                - invalid_payment_amount
                - payment_collection_not_enabled
                - payment_conflict
                - unknown_payment_option
                - payment_not_connected
                - sender_fields_empty
                - paygo_cap
                - envelope_not_draft
                - email_not_configured
                - idempotency_conflict
                - invite_delivery_failed
                - internal_error
            message:
              type: string
              description: Human-readable, echoes the offending value.
            fix:
              type: string
              description: What to do about it, in plain words.
            docs:
              type: string
              format: uri
              description: Deep link into https://gsdhere.com/developers.
        retry_after_seconds:
          type: integer
          description: On 429 only — seconds to wait before retrying.
    TemplateRole:
      type: object
      required: [key, label]
      properties:
        key:
          type: string
          description: Pass this as signers[].role when creating an envelope.
        label:
          type: string
    Template:
      type: object
      required: [id, name, created_at, roles, has_payment_defaults]
      properties:
        id:
          type: integer
        name:
          type: string
        created_at:
          type: string
          format: date-time
          nullable: true
        roles:
          type: array
          items:
            $ref: '#/components/schemas/TemplateRole'
        has_payment_defaults:
          type: boolean
          description: True when envelopes made from this template collect a payment by default.
        payment_options:
          type: array
          description: The template's owner-authored price menu — present only when the template has price options. Pass a key as payment_option when creating an envelope.
          items:
            $ref: '#/components/schemas/PriceOption'
    PriceOption:
      type: object
      required: [key, label, amount_usd]
      properties:
        key:
          type: string
          description: Lowercase snake slug — pass this as payment_option when creating an envelope.
        label:
          type: string
        amount_usd:
          type: number
          description: US dollars.
    EnvelopeSummary:
      type: object
      required: [id, name, status, template_id, created_at, sent_at, completed_at]
      properties:
        id:
          type: integer
        name:
          type: string
        status:
          type: string
          enum: [draft, sent, in_progress, completed, voided, expired]
        template_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        sent_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
    SignerPayment:
      type: object
      required: [required, status, amount_usd]
      properties:
        required:
          type: boolean
        status:
          type: string
          description: "'none' when no payment is required."
        amount_usd:
          type: number
          nullable: true
          description: US dollars.
    Signer:
      type: object
      required: [id, name, email, role, status, signing_order, viewed_at, signed_at, declined_at, payment]
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
        role:
          type: string
          nullable: true
          description: The template role's label.
        status:
          type: string
        signing_order:
          type: integer
        viewed_at:
          type: string
          format: date-time
          nullable: true
        signed_at:
          type: string
          format: date-time
          nullable: true
        declined_at:
          type: string
          format: date-time
          nullable: true
        payment:
          $ref: '#/components/schemas/SignerPayment'
    EnvelopeDetail:
      allOf:
        - $ref: '#/components/schemas/EnvelopeSummary'
        - type: object
          required: [expires_at, payment_status, signers]
          properties:
            expires_at:
              type: string
              format: date-time
              nullable: true
            payment_status:
              type: string
              enum: [none, pending, paid]
              description: Rollup across payment-required signers.
            signers:
              type: array
              items:
                $ref: '#/components/schemas/Signer'
    CreateSigner:
      type: object
      required: [role, name, email]
      properties:
        role:
          type: string
          description: A role key from the template (GET /api/v1/templates lists them). Exactly one signer per role; a role may not appear twice.
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: Appears on the invitation and the certificate.
        email:
          type: string
          format: email
          description: The signing invitation is emailed here.
        phone:
          type: string
          maxLength: 40
          description: Optional, for SMS delivery where enabled.
    CreateEnvelopeRequest:
      type: object
      required: [template_id, signers]
      properties:
        template_id:
          type: integer
          minimum: 1
          description: One of your template ids (GET /api/v1/templates).
        signers:
          type: array
          minItems: 1
          description: Exactly one entry per template role.
          items:
            $ref: '#/components/schemas/CreateSigner'
        prefill:
          type: object
          additionalProperties:
            type: string
          description: Values for the template's sender ("filled by you") fields, keyed by field name.
        message:
          type: object
          properties:
            subject:
              type: string
              maxLength: 300
            body:
              type: string
              maxLength: 5000
        payment_option:
          type: string
          description: Key of one of the template's price options (see the template's payment_options). The amount always comes from the option — mutually exclusive with payment (400 payment_conflict). An unknown key returns 404 unknown_payment_option listing the available keys.
        payment:
          type: object
          required: [amount_usd]
          description: Requires payment collection on your plan. Overrides the template's payment defaults. Mutually exclusive with payment_option.
          properties:
            amount_usd:
              type: number
              minimum: 0.5
              maximum: 10000
              description: US dollars, $0.50–$10,000.
            before_signing:
              type: boolean
              default: true
              description: Collect payment before the signer can sign.
            allow_offline:
              type: boolean
              description: Alternative to before_signing — true means the signer may sign first and pay offline.
        send:
          type: boolean
          default: false
          description: True to send immediately; false leaves a draft.
    CreateEnvelopeResponse:
      type: object
      required: [ok, envelope]
      properties:
        ok:
          type: boolean
          enum: [true]
        envelope:
          type: object
          required: [id, name, status, template_id, signers]
          properties:
            id:
              type: integer
            name:
              type: string
            status:
              type: string
              enum: [draft, sent]
            template_id:
              type: integer
            signers:
              type: array
              items:
                type: object
                required: [id, role, name, email]
                properties:
                  id:
                    type: integer
                  role:
                    type: string
                    description: The template role key.
                  name:
                    type: string
                  email:
                    type: string
                    format: email
            payment:
              type: object
              description: Present only when a payment override was given.
              properties:
                amount_usd:
                  type: number
                before_signing:
                  type: boolean
        send:
          type: object
          description: Present only when send was true.
          properties:
            sent:
              type: integer
            failed:
              type: array
              items:
                type: object
