Skip to main content
Developers

API & webhook reference

Pull your organization's proposals, milestones, and files programmatically, and get notified in real time via webhooks.

Getting an API key

Generate a key from the Developer section of Client Portal → Security. The raw key is shown once at creation — store it securely. Every request must include it as a Bearer token:

Authorization: Bearer aftpak_live_<your key>

Scopes

Each key is created as Read only or Read & write. Every GET endpoint below works with either scope; every POST endpoint requires a Read & write key and returns 403 otherwise.

Endpoints

GET /api/v1/proposals

curl -H "Authorization: Bearer aftpak_live_<key>" \
  https://aftpak.com/api/v1/proposals

Response

{
  "proposals": [
    {
      "id": "cljk...",
      "organizationId": "cljk...",
      "proposalCode": "PRP-104",
      "title": "...",
      "module": "...",
      "status": "Approved",
      "version": "v1.0",
      "submittedDate": "2026-06-01",
      "summary": "...",
      "scopeHighlights": ["..."],
      "approvedDate": "2026-06-10",
      "revisionNote": null,
      "updatedAt": "2026-06-10T12:00:00.000Z"
    }
  ]
}

POST /api/v1/proposals

Requires a Read & write key.

curl -X POST -H "Authorization: Bearer aftpak_live_<key>" \
  -H "Content-Type: application/json" \
  -d '{
    "proposalCode": "PRP-105",
    "title": "...",
    "module": "...",
    "summary": "...",
    "scopeHighlights": ["...", "..."]
  }' \
  https://aftpak.com/api/v1/proposals

Response — 201 Created

{ "proposal": { "id": "cljk...", "proposalCode": "PRP-105", ... } }

GET /api/v1/milestones

curl -H "Authorization: Bearer aftpak_live_<key>" \
  https://aftpak.com/api/v1/milestones

Response

{
  "milestones": [
    {
      "id": "cljk...",
      "organizationId": "cljk...",
      "phase": "Requirement Analysis",
      "status": "Completed",
      "description": "...",
      "date": "2026-06-01",
      "sortOrder": 1
    }
  ]
}

POST /api/v1/milestones

Requires a Read & write key.

curl -X POST -H "Authorization: Bearer aftpak_live_<key>" \
  -H "Content-Type: application/json" \
  -d '{
    "phase": "Deployment",
    "description": "...",
    "date": "2026-08-01",
    "sortOrder": 5
  }' \
  https://aftpak.com/api/v1/milestones

Response — 201 Created

{ "milestone": { "id": "cljk...", "phase": "Deployment", "status": "Pending", ... } }

GET /api/v1/files

curl -H "Authorization: Bearer aftpak_live_<key>" \
  https://aftpak.com/api/v1/files

Response

{
  "files": [
    {
      "id": "cljk...",
      "name": "Requirement_Analysis_Summary.pdf",
      "type": "PDF",
      "size": "220 KB",
      "uploadedDate": "2026-06-01",
      "phase": "Requirement Analysis"
    }
  ]
}

File metadata only — downloading file bytes requires an authenticated Client Portal session, not an API key.

Errors

A missing, invalid, or revoked API key returns:

HTTP/1.1 401 Unauthorized

{ "error": "Not authorized" }

A POST endpoint called with a Read only key returns:

HTTP/1.1 403 Forbidden

{ "error": "This API key does not have the required scope" }

Each API key is limited to 100 requests per minute. Exceeding it returns:

HTTP/1.1 429 Too Many Requests

{ "error": "Rate limit exceeded" }

Webhooks

Configure a webhook URL from the same Developer section to receive a signed POST request whenever one of the events below happens for your organization.

Event types

  • activity.created — new activity logged for your organization
  • invoice.paid — an invoice is marked as paid
  • milestone.completed — a project milestone is marked complete
  • ticket.created — a new support ticket is opened
  • ticket.replied — a support ticket gets a new reply, from either side
  • survey.responded — a client responds to a milestone satisfaction survey

Request body (activity.created example)

{
  "event": "activity.created",
  "payload": {
    "id": "cljk...",
    "organizationId": "cljk...",
    "date": "2026-06-01",
    "type": "proposal",
    "message": "...",
    "createdAt": "2026-06-01T12:00:00.000Z"
  },
  "timestamp": 1749000000000
}

Signature header — verify with the webhook secret shown at creation

X-Webhook-Signature: <hex HMAC-SHA256 of the raw request body>

Verifying in Node

const crypto = require("crypto");

function isValid(rawBody, signature, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return expected === signature;
}

See what's shipped recently in our Changelog.