CODEX PROMPT #2 — Add HubSpot Webhook + SCORM Callback Endpoints (Altus API Plugin)

  Dec 20th, 2025   -     Development with AI Resrouces, General API Instructions   -  

Below is the SECOND Codex Prompt (copy/paste into ChatGPT Codex in Visual Studio) to add two new endpoints to the existing altus-api plugin. It is written to match the v1 architecture you already defined (router/auth/db/response helpers), keep PHP 7.4.11 → 8.4 compatibility, and avoid schema changes. It also keeps security tight: HubSpot webhook signature verification + SCORM callback HMAC.       


CODEX PROMPT #2 — Add HubSpot Webhook + SCORM Callback Endpoints (Altus API Plugin)

Context

You are modifying an existing WordPress plugin:

/wp-content/plugins/altus-api/

The plugin already has:

  • REST namespace altus/v1

  • token auth via Authorization: Bearer {deacon_key} validated against {$acc_server_database}.acc_keys.deacon_key

  • DB wrapper class-altus-api-db.php with helper methods for contacts/courses/enrollments

  • response wrapper class-altus-api-response.php

Goal: Add two new endpoints:

  1. POST /wp-json/altus/v1/webhooks/hubspot/deal-refresh

  2. POST /wp-json/altus/v1/scorm/callback/complete

Hard requirements:

  • PHP 7.4.11 → 8.4 compatible

  • Prepared statements only

  • No DB schema changes

  • No token secrets exposed in logs/UI

  • Add logging entry in logs/update-log.txt following Altus Codex logging template 


A) Endpoint 1: HubSpot Webhook —

/webhooks/hubspot/deal-refresh

A1) Purpose

Receive a signed HubSpot webhook payload indicating a deal has been refreshed/updated and then:

  • upsert a Contact in Altus (acc_contacts + acc_contactsmeta)

  • create or update an Enrollment in Altus (ae_enrollments)

This endpoint is for HubSpot → Altus synchronization.

A2) Route

POST /wp-json/altus/v1/webhooks/hubspot/deal-refresh

A3) Authentication / Signature (REQUIRED)

This endpoint MUST NOT use the normal Bearer token.

Instead, it must validate a shared-secret HMAC signature header.

Implement a new WP option:

  • altus_hubspot_webhook_secret (string)

In wp-admin (optional for this phase): do not build UI, but code must support it being set.

Expected request header:

  • X-Altus-Signature: sha256={hex_hmac}

HMAC construction:

  • hex_hmac = hash_hmac(‘sha256’, raw_body, secret)

  • compare using hash_equals

If missing/invalid signature:

  • return 401 with altus_webhook_signature_invalid

Also require a replay-safe timestamp header:

  • X-Altus-Timestamp: {unix_seconds}

Rules:

  • timestamp must be within ±5 minutes of server time

  • if outside window return 401 altus_webhook_timestamp_invalid

Note: This avoids relying on HubSpot’s changing signature schemes. We are implementing our own signed webhook contract between HubSpot workflows/Zapier and Altus.

A4) Payload Format (What to Expect)

Accept JSON body with at least:

{
  "event": "deal.refresh",
  "source": "hubspot",
  "master_key": "a1060911",
  "deal": {
    "deal_id": "123456789",
    "deal_name": "ImagingCampus Renewal - ABC Imaging",
    "deal_stage": "closedwon",
    "amount": "1999.00",
    "close_date": "2025-12-20"
  },
  "contact": {
    "email": "user@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "phone": "555-555-5555",
    "organization": "ABC Imaging",
    "altus_role": "Customer"
  },
  "enrollment": {
    "course_id": 2810,
    "blog_master_key": "i0463709",
    "transaction_id": "HS-DEAL-123456789",
    "enrolled": 1
  }
}

Notes:

  • master_key will usually be a1060911 but should not be hardcoded. 

  • contact email is the primary lookup key.

  • enrollment must include course_id and blog_master_key (do not guess blog_master_key).

  • deal data should be stored (if at all) only as contact meta keys, not new DB tables.

A5) Mapping Rules

Contact upsert

  • If email exists → update the contact

  • Else create new contact

  • Update meta:

    • hubspot_deal_id = deal_id

    • hubspot_deal_stage = deal_stage

    • hubspot_deal_name = deal_name

    • hubspot_last_sync = ISO timestamp

    • altus_role if provided

    • ae_organization if you can map org safely; otherwise store hubspot_company_name

(Do not create new tables. Only meta in acc_contactsmeta.) 

Enrollment create/update

  • Find existing enrollment by (contact_id, course_id, blog_master_key)

  • If exists, update:

    • transaction_id if provided

    • enrolled if provided

  • If not exists, insert new row with enrollment_date NOW and enrolled=1

Use ae_enrollments. 

A6) Response

Return wrapped JSON:

{
  "ok": true,
  "data": {
    "contact_id": 77590,
    "enrollment_id": 12345,
    "action": "updated"
  },
  "meta": { ... }
}

If any operation fails:

  • return error wrapper with request_id

  • do not reveal SQL or secrets


B) Endpoint 2: SCORM Completion Callback —

/scorm/callback/complete

B1) Purpose

SCORM packages exported from AltusLMS (hosted elsewhere) call back to Altus when a learner completes:

  • course completion

  • evaluation completion (optional)

  • credit claim (optional)

This endpoint updates ae_enrollments fields accordingly.

B2) Route

POST /wp-json/altus/v1/scorm/callback/complete

B3) Authentication (REQUIRED)

Also must NOT use normal Bearer token.

Use a WP option:

  • altus_scorm_callback_secret

Require headers:

  • X-Altus-Signature: sha256={hex_hmac}

  • X-Altus-Timestamp: {unix_seconds} (±5 minutes)

    Same signature logic as HubSpot:

  • HMAC over raw request body

  • hash_equals compare

If missing/invalid:

  • 401

B4) Payload Format (What to Expect)

Accept JSON body:

{
  "event": "scorm.complete",
  "source": "scorm",
  "master_key": "a1060911",
  "blog_master_key": "i0463709",
  "course_id": 2810,
  "contact": {
    "email": "user@example.com"
  },
  "completion": {
    "completed": true,
    "course_completion_date": "2025-12-20",
    "evaluation_completed": true,
    "evaluation_completed_date": "2025-12-20",
    "received_credit": 1
  },
  "attempt": {
    "external_attempt_id": "SCORM-ATTEMPT-abc123",
    "score": 92,
    "passed": true
  }
}

Rules:

  • identify user by email → resolve to contact_id via acc_contacts.primary_email

  • must have course_id + blog_master_key

  • if enrollment does not exist, create it first (enrolled=1)

B5) Enrollment Updates

Update allowed fields only (match existing columns in ae_enrollments usage): 

  • course_completion_date

  • ae_course_completed (set to 1 when completed true)

  • ae_evaluation_completed (set to 1 when evaluation_completed true)

  • ae_evaluation_completed_date

  • received_credit

If payload includes completion.course_completion_date use it; otherwise use NOW.

Store SCORM attempt tracking in acc_contactsmeta or ae_enrollments only if there is a safe existing column; otherwise use acc_contactsmeta:

  • scorm_last_attempt_id

  • scorm_last_score

  • scorm_last_passed

  • scorm_last_callback_at

(No schema changes.) 

B6) Response

Return:

  • contact_id

  • enrollment_id

  • updated fields summary


C) Implementation Tasks (Exact Code Changes)

C1) Add Two New Endpoint Classes

Create:

  • endpoints/class-altus-api-webhooks-hubspot.php

  • endpoints/class-altus-api-scorm.php

And require them from altus-api.php (or from router).

C2) Router Updates

In class-altus-api-router.php, register:

  • /webhooks/hubspot/deal-refresh (POST)

  • /scorm/callback/complete (POST)

Permission callbacks for these two must NOT use Bearer token auth.

Instead, add a new verifier class OR methods inside a new file:

includes/class-altus-api-signature.php with:

  • verify_signed_request(WP_REST_Request $request, string $secret_option_key): array

    • reads raw body

    • reads timestamp header

    • validates ±5 minutes

    • validates signature header

    • returns ok/error WP_Error

C3) DB Helper Enhancements

If not already available, add methods in class-altus-api-db.php:

  • resolve_contact_id_by_email(string $email): ?int

  • upsert_contact_from_webhook(array $contact_payload, array $meta): int

  • create_or_update_enrollment_by_keys(int $contact_id, int $course_id, string $blog_master_key, array $fields): array

All use prepared statements, whitelist columns.

C4) Input Validation

  • Validate email with is_email

  • Validate course_id/contact_id numeric

  • Require blog_master_key for both webhook and scorm endpoints

  • Sanitize all strings

C5) Error Handling

Return errors using existing response wrapper:

  • Altus_API_Response::error($wp_error)

Do not expose raw SQL errors.

C6) Add Curl Examples (in code comments or readme)

Add example curl calls with signature creation notes.


D) Curl Example (HubSpot webhook simulation)

Provide this in the PR output:

  1. Create body JSON file payload.json

  2. Generate timestamp

  3. Generate signature:

TS=$(date +%s)
SIG=$(php -r '$b=file_get_contents("payload.json"); $s=getenv("SECRET"); echo hash_hmac("sha256",$b,$s);' )
curl -sS -X POST "https://YOURDOMAIN/wp-json/altus/v1/webhooks/hubspot/deal-refresh" \
  -H "Content-Type: application/json" \
  -H "X-Altus-Timestamp: $TS" \
  -H "X-Altus-Signature: sha256=$SIG" \
  --data-binary @payload.json

E) Logging Update

Append to:

/wp-content/plugins/altus-api/logs/update-log.txt

  • CATEGORY: UPDATE

  • FILES UPDATED: list new files

  • DB IMPACT: None (meta keys added are not schema changes, but mention new meta usage if you add any)

  • LINKS: optional

Follow standard format. 


F) Acceptance Criteria (Must Pass)

  1. Both endpoints exist and return JSON wrapper responses.

  2. Both endpoints reject:

    • missing signature

    • invalid signature

    • invalid timestamp

  3. HubSpot webhook:

    • upserts contact by email

    • upserts enrollment by (contact_id, course_id, blog_master_key)

  4. SCORM callback:

    • resolves contact by email

    • creates enrollment if missing

    • updates completion/evaluation/credit fields correctly

  5. No schema changes.

  6. Compatible with PHP 7.4.11 → 8.4.


Implement now. Do not ask questions. Use safe defaults:

  • require blog_master_key in both payloads (do not guess)

  • store external ids as contact meta keys if needed (no new tables)

Begin coding changes now.



Your Comment

Your email address will not be published.