Creating Webhooks
Creating a webhook is a two-step process. You'll first need to set up how you want your webhook to behave through CreativeForce - what events should it listen to. After that, you'll set up your server to receive and manage the payload.
Setting up a Webhook
To set up a webhook, go to the “Studio settings” page via the menu in the top right corner, and click on "Integration" and from there on the sub-menu "Webhooks":
From there, click “Add webhook”. Webhooks require a few configuration options before you can make use of them. We'll go through each of these settings below here.
Set up
Payload URL
The Payload URL is the URL of the server that will receive the webhook requests.
This URL must be HTTPS
The URL can not be an IP (private IP as well as public IP)
The URL can not contain any of the following:
localhost
creativeforce.io
metadata.google
burpcollaborator.net
.local
.internal
ram.aliyuncs.com
fuf.me
localtest.me
ulh.us
Example of a good Payload URL
Examples of bad Payload URLs
https://localhost:4567/payload
http://localhost:8000/payload
Secret
When your secret key is set, Creative Force uses it to create a hash signature with each payload. This hash signature is passed along with each request in the headers as X-CF-Signature. See the section about validating the webhook later in this document.
The format of the secret key is Base64 RFC 2045
Events to trigger the webhook
Events are at the core of webhooks. The webhooks fire whenever a certain action is taken either by users or the Creative Force system. Your server (payload URL) will intercept and act upon these.
You can select from 4 different event groups:
Product Events
Imported
Reset
Deleted
StatusChanged
StyleguideChanged
CategoryChanged
Ignored
ColorReferenceMigrated
PropertiesUpdated
Activated
Color Reference Created
Color Reference Updated
Color Reference Deleted
Move To Another Job
Sample Events
LocationChanged
Checked-in
Checked-out
User Events
UserLoggedIn
UserLoggedOut
PasswordChanged
AvatarUpdated
UserInvitationCompleted
UserUpdated
UserDeleted
UserInvitationDeleted
UserDisabled
UserEnabled
UserInvited
UserRe-Invited
Task Events
Completed
Started
ReadyToWork
Bypassed
Reset
Assigned
Unassigned
Rejected
Approved
Process without Color Reference
Failed
Copywriting Task Edit Made
WorkUnit Events
StatusChanged
Reset
Created
Deleted
Enabled
Disabled
ChangedSource
Completed
Bypass External Post
Change Post Vendor
Job Events
Job Created
Job Deleted
Job Updated
Job Status Changed
Job Deadline Updated
Session Events
Add To Session
When you're finished, click Save. Now that you created the webhook, it's time to set up our server to test the webhook.
Webhook Details
Delivery headers
HTTP POST payloads that are delivered to your webhook's configured URL endpoint will contain several special headers:
Header | Description |
X-CF-Event | Name of the event that triggered the delivery. For example “Task completed”, “User created”. |
X-CF-Signature | The HMAC hex digest of the response body. See the section about validating the webhook later in this document. |
Webhook for “Product”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: Imported, Reset, Deleted, StatusChanged, StyleGuideChanged, CategoryChanged, Ignored, ColorReferenceMigrated, PropertiesUpdated, Activated, Completed, MoveToAnotherJob |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: product |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
JobId | string | ID of the job (GUID) |
JobCode | string | Job code |
ClientId | string | ID of the client (GUID). |
ClientName | string | Client name |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
ProductStatusId | int | Product Unit Status ID |
ProductStatusName | string | Product Unit Status Name |
Webhook payload example when someone reset a Product:
{
"Action":"ProductReset",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"product",
"EventDatetimeUtc":1595204418000,
"JobId":"d84eef10-c8a6-437a-9f95-0b58d2a3cb38",
"JobCode":"Summer 2020",
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
"ProductId":"7a782d57-b0bf-4743-8f02-2b1a1e229869",
"ProductCode":"AW2130146"
}
Webhook for “Sample”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: LocationChanged, Checked-in, Checked-out |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: sample |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
SampleId | string | ID of the Sample (GUID) |
SampleCode | String | Sample Code |
LocationId | string | ID of the location (GUID) |
LocationName | string | Location name |
SubLocationId | string | ID of the sub-location (GUID) |
SubLocationName | string | Sub-location name |
Products | array | Array of ID of the product ID (GUID) and Product code. If there is no product then return an empty array. products: [{productId: "", productCode: ""},....} |
Webhook payload example when someone check-in a sample:
{
"SampleId": "4666fcd0-0179-44d8-a66d-bc65a4acd968",
"SampleCode": "88829384",
"LocationId": "ee410fb0-6a99-4b0e-a95e-83abe6aa5a5e",
"LocationName": "CF Center",
"SubLocationId": "00000000-0000-0000-0000-000000000000",
"SubLocationName": "",
"Products": [{
"ProductId": "ef0533fc-a11c-43a2-ab84-1c9bdabc9e7f",
"ProductCode": "ADROAN-830"
}
],
"PayloadId": "5385f2a9-88da-4c38-88a5-5f6f875ecb74",
"EventGroupName":"sample",
"EventDatetimeUtc": 1601969236991,
"Action": "Checked-in",
"Actor": {
"UserId": "205c0bbd-ea29-4250-87f5-bdb254a91075"
}
}
Webhook for “User”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: UserLoggedIn, UserLoggedOut, PasswordChanged, AvatarUpdated, UserInvitationCompleted, UserUpdated, UserDeleted, UserInvitationDeleted, UserDisabled, UserEnabled, UserInvited, UserRe-Invited |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: user |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
UserId | string | ID of the user (GUID) Notes: We will send UserID only, no user name. |
Webhook payload example when a user login:
{
"Action":"UserLoggedIn",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"user",
"EventDatetimeUtc":1595204418000,
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe",
}
Webhook for “Task”
Key | Type | Description |
Action | string | The action that was performed. Can be one of: Completed, Started, ReadyToWork, Bypassed, Reset, Assigned, Unassigned, Rejected, Approved, Failed |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: task |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
TaskId | string | ID of the task (GUID) |
Assignee | object | The assignee of this task. Notes: We will send UserID only, no user name. |
JobId | string | ID of the job (GUID) |
JobCode | string | Job code |
ClientId | string | ID of the client (GUID). |
ClientName | string | Client name |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
StepId | int | ID of the step |
StepName | string | Step name |
StepStatusId | int | ID of the Step Status |
StepStatusName | string | Step Status name |
ShootingTypeId | int | ID of the Shooting type (aka Production type) |
ShootingTypeName | string | Shooting type name (aka Production type name) |
WorkflowId | string | ID of the Workflow |
WorkflowName | string | Workflow name |
StyleGuideId | string | ID of the Style guide |
StyleGuideName | string | Style guide name |
Webhook payload example when someone finishes a Photography task:
{
"Action":"completed",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"task",
"EventDatetimeUtc":1595204418000,
"TaskId":"c02f0f73-4665-4aae-aac1-c2fbc65365de",
"Assignee":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"JobId":"d84eef10-c8a6-437a-9f95-0b58d2a3cb38",
"JobCode":"Summer 2020",
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
"ProductId":"7a782d57-b0bf-4743-8f02-2b1a1e229869",
"ProductCode":"AW2130146",
"StepId":"3",
"StepName":"Photography",
"StepStatusId":"9000",
"StepStatusName":"Done",
"ShootingTypeId":"1",
"ShootingTypeName":"On Model",
"WorkflowId":"2b5d3e1b-1706-4973-9a4d-57ab7d6685e9",
"WorkflowName":"My first workflow",
"StyleGuideId":"2376b9cb-67db-490f-a5e8-89811ed6b087",
"StyleGuideName":"Summer on the beach 2020"
}
Webhook for Work Unit
Key | Type | Description |
Action | string | The action that was performed. Can be one of: StatusChanged, Reset, Created, Deleted, Enabled, Disabled, ChangedSource, Completed |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. For some following cases when data is updated by background service, then Actor is empty:
* Sync from data source: create/delete product
|
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: work unit |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
ClientId | string | ID of the client (GUID). |
ClientName | string | Client name |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
ProductionTypeId | int | Production Type ID |
ProductionTypeName | string | Production Type Name |
WorkUnitStatusId | int | Work Unit Status ID after changed |
WorkUnitStatusName | string | Work Unit Status Name after changed |
Webhook payload example when a work unit status changed:
{
"Action":"StatusChanged",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"work unit",
"EventDatetimeUtc":1595204418000,
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
"ProductId":"7a782d57-b0bf-4743-8f02-2b1a1e229869",
"ProductCode":"AW2130146"
"ProductionTypeId":1,
"ProductionTypeName":"OnModel"
"WorkUnitStatusId":3000
"WorkUnitStatusName":"InProgress"
}
Webhooks for Jobs
Key | Type | Description |
Action | string | The action that was performed. Can be one of: Created, Updated, Deleted, StatusChanged, DeadlineUpdated |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: Job |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
JobId | string | ID of the job (GUID) |
JobName | string | Job name |
JobCode | string | Job code |
JobStatusId | int | Job Unit Status ID |
JobStatusName | string | Job Unit Status Name |
ClientId | string | ID of the client (GUID) |
ClientName | string | Client Name |
JobDeadlineUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
Webhook payload example when someone deletes a Job:
{
"Action":"Deleted",
"Actor":{
"UserId":"19e62847-f302-4e32-9e4a-b6d8c304fcbe"
},
"PayloadId":"c02f0f73-1706-4973-9a4d-38007d6685e9",
"EventGroupName":"Job",
"EventDatetimeUtc":1595204418000,
"JobId":"d84eef10-c8a6-437a-9f95-0b58d2a3cb38",
“JobName:”Summer 2020”,
"JobCode":"Summer 2020”,
“JobStatusId”:3000,
“JobStatusName”:”In Progress”,
"ClientId":"2c856b2f-aded-4609-bf67-27009603b2b9",
"ClientName":"My Big client",
“JobDeadlineUtc”:1595204529000,
}
Webhook for Session
Key | Type | Description |
Action | string | The action that was performed. Can be one of: AddedToSession |
Actor | object | The user or the actor that triggered the event. Notes: We will send UserID only, no user name. |
PayloadId | string | ID of the payload (GUID) |
EventGroupName | string | The event group triggered. Value: session |
EventDatetimeUtc | long | Unix Timestamp (milliseconds) - the number of milliseconds since the Unix Epoch |
ProductId | string | ID of the product (GUID) |
ProductCode | string | Product code |
ShootingTypeId | int | ID of the Shooting type (aka Production type) |
ShootingTypeName | string | Shooting type name (aka Production type name) |
WorkUnitId | string | ID of the WorkUnit (GUID) |
SessionId | string | ID of the Session (GUID) |
SessionCode | string | Session Code |
SessionName | string | Session Name |
PlannedTimeSlot | JSON literal | Time of Session |
PlannedTeam | JSON literal | PlannedTeam consists of individuals participating in that session and specifies who they are and their respective roles. |
PlannedProducerId | string | ID of the Planned Producer (GUID) |
PlannedProducerName | string | Planned Producer Name |
PlannedSetId | string | ID of the Planned Set (GUID) |
ProductionItemId
| string | ID of the Production Item (GUID) |
PlanningStatus
| JSON literal | Status (ID, Name) of the planning Can be one of: Unplanned = 2, Planned = 1 |
ActionSource | string | The app that triggered the event |
Webhook payload example when someone Add a Session:
{
"ActionSource": "AddFromGateway",
"SessionId": "cba7640d-452b-417b-8bbc-aaa92a763359",
"SessionCode": "SES113736CF",
"SessionName": "Quyen Test Add session",
"PlannedTimeSlot": "[{\"PlannedStart\":\"2023-12-10T01:00:00\",\"PlannedEnd\":\"2023-12-10T10:00:00\"}]",
"PlannedTeam": "{\"Assistant\":\"phuonglth+dev@creativeforce.io,dungnt+sing@creativeforce.io\",\"Photographer\":\"hanh@creativeforce.io\",\"HairAndMakeup\":\"duongvtt+dev@creativeforce.io\",\"DigitalProcessing\":\"hanttn@creativeforce.io\"}",
"PlannedProducerId": "429f37e8-4583-4541-89d9-49230bbf046f",
"PlannedProducerName": "DuongVTT DEV",
"PlannedSetId": "53c4bc2d-244a-490e-9f75-e3554246b6dd",
"ProductionItemId": "1e6b01e0-671e-462c-b266-692d6a16eb92",
"PlanningStatus": "{\"PlanningStatusId\":2,\"PlanningStatus\":\"Unplanned\"}",
"WorkUnitId": "1e6b01e0-671e-462c-b266-692d6a16eb92",
"ShootingTypeId": 1,
"ProductId": "99ecc381-41f1-4782-bcd4-e762e50a8a93",
"ProductCode": "Product_230831_5",
"PayloadId": "d483e7cb-e03b-4ccd-b307-22ca3e990e96",
"EventGroupName": "session",
"EventDatetimeUtc": 1701935735820,
"Action": "AddedToSession",
"Actor": {
"UserId": "600a5a14-8bd5-4d48-888e-f2b0bf2defc8"
}
}
Security
Validating the webhooks
The way to validate the webhook in your application is to hash the JSON payload of the webhook using the SHA256 algorithm and the secret key generated when you set up the webhook in gamma.
You will then compare the hash with the webhook signature. If they match, the webhook can be trusted.
The X-CF-Signature is 32 bytes and in hexa format. It's therefore important to make sure your hash function is returning 32-byte hexa as well.
Using a plain == operator is not advised. A method like secure_compare performs a "constant time" string comparison, which renders it safe from certain timing attacks against regular equality operators.
Below is an example of this process. The example is written in Ruby for a Sinatra server but should give you a good idea of the approach.
require 'sinatra'
require 'json'
post '/payload' do
request.body.rewind
payload_body = request.body.read
verify_signature(payload_body)
push = JSON.parse(payload_body)
puts "I got callback, JSON: #{push}"
return halt 200, "OK"
end
def verify_signature(payload_body)
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA256'), ENV['SECRET_TOKEN'], payload_body)
return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_CF_SIGNATURE'])
end
Other
Content-Type
Creative Force always uses application/json as the content type.
Enable/Disable
You can choose to disable a webhook if you want to pause it temporarily.
Retry
The CF system will POST data to the payload URL specified and expects a 2xx HTTP status code return. If no 2xx HTTP status is returned, CF system will automatically retry (3 times only), each after 5 - 10 minutes.